Develop PCF Control to show activities in a sub-grid on account form from related contacts using MS Fluent UI and React

Introduction

When it comes to CRM systems, it’s common to display activities associated with contacts on the parent account form using timelines and associated grids through the roll-up view. However, sometimes, business requirements call for a more customized approach. Such was the case when we needed to exhibit contact-related activities directly on the account form within a dedicated section or sub-grid.

In this blog post, we embark on a journey to fulfill this requirement by creating a PCF (Power Apps Component Framework) control. Our objective is clear: we aim to showcase activities on the account form that stem from related contacts. Along the way, we’ll delve into the intricacies of leveraging Microsoft Fluent UI and harnessing the power of React within the PCF Control.

Join us on this exciting exploration as we unlock the potential of displaying activities in a sub-grid on the account form, all while seamlessly integrating Microsoft Fluent UI and React into our PCF Control for a comprehensive and tailored solution.

Prerequisites

Steps To Create The PCF Project

Following are the step-by-step guide to develop the project. 

Get to know about the step-by-step guide to create custom PCF control.

Setup The PCF Project And Install MS Fluent UI Components

Create a folder for the PCF Control project as D:/PCFControls/RelatedActivitiesGrid.

Open Developer PowerShell. 

  • Run a command for change directory to D:/PCFControls/RelatedActivitiesGrid.

cd D:/PCFControls/RelatedActivitiesGrid

  • Run Command for creating PCF Project.

pac pcf init –namespace xRM.Demo.PCFControls –name RelatedActivitiesGrid –template field

  • Run Command “npm install” for Installing project dependencies.

npm install

  • Run the following command for installing Fluent UI components, overview link.

npm i office-ui-fabric-react @fluentui/react

  • Run the following command to build the project.

npm run build

You can view all the commands below in the screenshot.

Elevate Your Software Solutions with AlphaBOLD

Elevate your software solutions with our bespoke development services. Let's discuss how we can bring your vision to life.

Request a Consultation

PCF Control Project Hierarchy

Open the folder and confirm the following folders/files are created after running the above commands. 

Open The PCF Project In Visual Studio Code

Right-click the folder “D:/PCFControls/RelatedActivitiesGrid” and open with VS Code.

Update ControlManifest.Inpt.Xml

Add a new property with the  name “accountid” for input usage. Uncomment the last two features, “utility” and “WebAPI” as we will need to use these features in our code.

<?xml version=”1.0″ encoding=”utf-8″ ?> 

<manifest> 

  <control namespace=”xRM.Demo.PCFControls” constructor=”RelatedActivitiesGrid” version=”0.0.1″ display-name-key=”RelatedActivitiesGrid” description-key=”RelatedActivitiesGrid description” control-type=”standard”> 

   <property name=”sampleProperty” display-name-key=”Property_Display_Key” description-key=”Property_Desc_Key” of-type=”SingleLine.Text” usage=”bound” required=”true” /> 

   <property name=”accountid” display-name-key=”accountid_Name_Key” description-key=”accountid_Desc_Key” of-type=”SingleLine.Text” usage=”input” required=”true” /> 

   <resources> 

      <code path=”index.ts” order=”1″/>     

    </resources>   

    <feature-usage>    

      <uses-feature name=”Utility” required=”true” /> 

      <uses-feature name=”WebAPI” required=”true” /> 

    </feature-usage>     

  </control> 

</manifest> 

Add A New File DetailsList-Simple.Tsx Under The RelatedActivitiesGrid Folder:

Add following code to DetailList-Simple.tsx file.

import * as React from ’react’; 

import { Fabric } from ’office-ui-fabric-react/lib/Fabric’; 

import {  DetailsList,  DetailsListLayoutMode,  SelectionMode,  IColumn,  } from ’office-ui-fabric-react/lib/DetailsList’; 

import { Link } from ’office-ui-fabric-react/lib/Link’; 

import { initializeIcons } from ’@uifabric/icons’; 

import Stack from ’office-ui-fabric-react/lib/components/Stack/Stack’;

import { Text } from ’office-ui-fabric-react/lib/Text’; 

initializeIcons(); 

export interface IDetailsListState { 

  columns: IColumn[]; 

  items: IDetailsListItem[];  

  announcedMessage?: string;   

export interface IDetailsListItem { 

  key: string; 

  subject: string; 

  regardingobjectid:string; 

  activitytypecode: string; 

  statecode: string; 

  ownerid:string;  

  scheduledend: string; 

  createdon: string; 

  modifiedon: string; 

export class ActivitiesGrid extends React.Component<any, IDetailsListState> { 

  private _allItems: IDetailsListItem[]; 

  private _pcfContext:any; 

  private _accountid: string; 

  private _announcedMessage: string=”loading….”; 

  constructor(props: any) { 

    super(props); 

    this._pcfContext= props.pcfContext; 

    this._accountid = props.accountid; 

    const _relatedContactActivitiesfetchXml: string =”<fetch version=’1.0’ output-format=’xml-platform’ mapping=’logical’ distinct=’false’>”+ 

    ”   <entity name=’activitypointer’>”+ 

    ”      <attribute name=’subject’ />”+ 

    ”      <attribute name=’ownerid’ />”+ 

    ”      <attribute name=’regardingobjectid’ />”+ 

    ”      <attribute name=’activitytypecode’ />”+ 

    ”      <attribute name=’statecode’ />”+ 

    ”      <attribute name=’scheduledstart’ />”+ 

    ”      <attribute name=’scheduledend’ />”+ 

    ”      <attribute name=’instancetypecode’ />”+ 

    ”      <attribute name=’modifiedon’ />”+ 

    ”      <attribute name=’createdon’ />”+ 

    ”      <attribute name=’activityid’ />”+ 

    ”      <order attribute=’modifiedon’ descending=’true’ />”+ 

    ”      <filter type=’and’>”+ 

    ”        <condition attribute=’isregularactivity’ operator=’eq’ value=’1’ />”+                     

    ”      </filter>”+                       

    ”      <link-entity name=’contact’ from=’contactid’ to=’regardingobjectid’ link-type=’inner’ alias=’ldc’>”+ 

    ”        <filter type=’and’>”+ 

    ”          <condition attribute=’parentcustomerid’ operator=’eq’ value=’”+this._accountid+”‘ />”+ 

    ”        </filter>”+ 

    ”      </link-entity>”+ 

    ”    </entity>”+ 

    ”  </fetch>”; 

    const _accountActivitiesfetchXml: string =”<fetch version=’1.0’ output-format=’xml-platform’ mapping=’logical’ distinct=’false’>”+ 

    ”   <entity name=’activitypointer’>”+ 

    ”      <attribute name=’subject’ />”+ 

    ”      <attribute name=’ownerid’ />”+ 

    ”      <attribute name=’regardingobjectid’ />”+ 

    ”      <attribute name=’activitytypecode’ />”+ 

    ”      <attribute name=’statecode’ />”+ 

    ”      <attribute name=’scheduledstart’ />”+ 

    ”      <attribute name=’scheduledend’ />”+ 

    ”      <attribute name=’instancetypecode’ />”+ 

    ”      <attribute name=’modifiedon’ />”+ 

    ”      <attribute name=’createdon’ />”+ 

    ”      <attribute name=’activityid’ />”+ 

    ”      <order attribute=’modifiedon’ descending=’true’ />”+ 

    ”      <filter type=’and’>”+ 

    ”        <condition attribute=’isregularactivity’ operator=’eq’ value=’1’ />”+   

    ”        <condition attribute=’regardingobjectid’ operator=’eq’  value=’”+this._accountid+”‘ />”+ 

    ”      </filter>”+   

    ”    </entity>”+ 

    ”  </fetch>”; 

 

    let _accountActivitiesItems: any[]=[]; 

    let _relatedContactActivitiesItems: any[]=[]; 

    let _allActivities: any[]=[]; 

    this._pcfContext.webAPI.retrieveMultipleRecords(‘activitypointer’,”?fetchXml=”+encodeURIComponent(_accountActivitiesfetchXml)).then( 

      (results: any) => {  

        _accountActivitiesItems=this.populateRecords(results);       

      this._pcfContext.webAPI.retrieveMultipleRecords(‘activitypointer’,”?fetchXml=”+encodeURIComponent(_relatedContactActivitiesfetchXml)).then( 

            (results: any) => {               

              _relatedContactActivitiesItems=this.populateRecords(results);

              _allActivities=_relatedContactActivitiesItems.concat(_accountActivitiesItems); 

              this._allItems=_allActivities.sort((a: { modifiedon_Value: number; }, b: { modifiedon_Value: number; }) => b.modifiedon_Value-a.modifiedon_Value);  

              if(this._allItems==null || this._allItems.length>0) 

              { 

                this.setState({ items: this._allItems });  

              }else{ 

                this.setState({announcedMessage: ”No data found”});  

              } 

            }, 

            (error: any) => { 

              this.setState({announcedMessage: ”Error while fetching records”});  

            }       

          );        

      }, 

      (error: any) => {  

        this.setState({ 

          announcedMessage: ”Error while fetching records” 

        });  

      } 

    ); 

    this._allItems=_allActivities; 

    const columns: IColumn[] = [       

        { key: ’subject’, name: ’Subject’, fieldName: ’subject’,  isRowHeader: true, minWidth: 100, maxWidth: 200, isResizable: true, 

        onRender: item => (           

          <Link key={item} onClick={() => this._pcfContext.navigation.openForm({entityName: item.activitytypecode_Value,entityId:item.key})}> 

            {item.subject} 

          </Link> 

        ), onColumnClick: this._onColumnClick },       

        { key: ’regardingobjectid’, name: ’Regarding’, fieldName: ’regardingobjectid’, minWidth: 100, maxWidth: 200, isResizable: true, 

        onRender: item => (           

          <Link key={item} onClick={() => this._pcfContext.navigation.openForm({entityName: item.regardingobjectid_LookupLogicalName,entityId:item.regardingobjectid_Guid})}> 

            {item.regardingobjectid} 

          </Link> 

        ),onColumnClick: this._onColumnClick },    

        { key: ’activitytypecode’, name: ’Activity Type’, fieldName: ’activitytypecode’, minWidth: 100, maxWidth: 200, isResizable: true ,onColumnClick: this._onColumnClick}, 

        { key: ’statecode’, name: ’Activity Status’, fieldName: ’statecode’, minWidth: 100, maxWidth: 200, isResizable: true ,onColumnClick: this._onColumnClick},

        { key: ’ownerid’, name: ’Owner’, fieldName: ’ownerid’, minWidth: 100, maxWidth: 200, isResizable: true, 

        onRender: item => (           

          <Link key={item} onClick={() => this._pcfContext.navigation.openForm({entityName: item.ownerid_LookupLogicalName,entityId:item.ownerid_Guid})}> 

            {item.ownerid} 

          </Link> 

        ),onColumnClick: this._onColumnClick },       

        { key: ’scheduledend’, name: ’Due Date’, fieldName: ’scheduledend_Value’, minWidth: 100, maxWidth: 200, isResizable: true , 

        data: ’number’,onRender: (item: IDetailsListItem) => {return <span>{item.scheduledend}</span>; }, onColumnClick: this._onColumnClick }, 

        { key: ’createdon’, name: ’Created On’, fieldName: ’createdon_Value’,minWidth: 100, maxWidth: 200, isResizable: true, 

         data: ’number’,onRender: (item: IDetailsListItem) => {return <span>{item.createdon}</span>; }, onColumnClick: this._onColumnClick }, 

         { key: ’modifiedon’, name: ’Last Modified’, fieldName: ’modifiedon_Value’,minWidth: 100, maxWidth: 200, isResizable: true,isSorted: true,isSortedDescending: true,sortAscendingAriaLabel: ’Sorted A to Z’, sortDescendingAriaLabel: ’Sorted Z to A’, 

         data: ’number’,onRender: (item: IDetailsListItem) => {return <span>{item.modifiedon}</span>; }, onColumnClick: this._onColumnClick}       

    ];    

    this.state = { 

      items: this._allItems, 

      columns: columns,       

      announcedMessage: this._announcedMessage 

    }; 

  } 

  public render(): JSX.Element { 

    const { columns,  items,  announcedMessage } = this.state; 

    return (   

      <Fabric>  

        <div style={{ position: ’relative’, maxHeight: ’400px’, overflow: ”auto” }}> 

          <DetailsList 

            items={items}            

            columns={columns} 

            selectionMode={SelectionMode.none} 

            getKey={this._getKey} 

            setKey=”none” 

            layoutMode={DetailsListLayoutMode.justified}   

           isHeaderVisible={true} 

            onItemInvoked={(item:any)=>{               

                this._pcfContext.navigation.openForm( 

                    { 

                      entityName: item.activitytypecode_Value, 

                      entityId: item.key 

                    } 

                  ); 

             } 

            } 

          />   

           { !this.state.items.length && ( 

              <Stack horizontalAlign=’center’> 

              <Text>{announcedMessage}</Text> 

            </Stack> 

              )} 

          </div>        

      </Fabric>    

    ); 

  } 

  public componentDidUpdate(previousProps: any, previousState: IDetailsListState) { 

 

  } 

 

  private populateRecords(results:any): any{  

    let _allItems: any[]=[]; 

    for (let i = 0; i < results.entities.length; i++) { 

      let e=results.entities[i]; 

      _allItems.push({ 

      key: e.activityid,  

      subject: e.subject,      

      regardingobjectid_Guid:e[“_regardingobjectid_value”],      

      regardingobjectid:e[“_regardingobjectid_value@OData.Community.Display.V1.FormattedValue”], 

      regardingobjectid_LookupLogicalName:e[“_regardingobjectid_value@Microsoft.Dynamics.CRM.lookuplogicalname”], 

      activitytypecode_Value:e.activitytypecode, 

      activitytypecode: e[“[email protected]”], 

      statecode: e[“[email protected]”],   

 

      ownerid_Guid:e[“_ownerid_value”],      

      ownerid:e[“[email protected]”], 

      ownerid_LookupLogicalName:e[“[email protected]”], 

      scheduledstart:( e.scheduledstart!=null?new Date(e.scheduledstart).toLocaleDateString():””),    

      scheduledstart_Value:( e.scheduledstart!=null?new Date(e.scheduledstart).getTime():0), 

     scheduledend:( e.scheduledend!=null?new Date(e.scheduledend).toLocaleDateString():””),  

      scheduledend_Value:( e.scheduledend!=null?new Date(e.scheduledend).getTime():0), 

      createdon: ( e.createdon!=null?new Date(e.createdon).toLocaleDateString():””),  

      createdon_Value:( e.createdon!=null?new Date(e.createdon).getTime():0), 

      modifiedon: ( e.modifiedon!=null?new Date(e.modifiedon).toLocaleDateString():””), 

      modifiedon_Value:( e.modifiedon!=null?new Date(e.modifiedon).getTime():0) 

      });   

    } 

    return _allItems; 

  } 

  private _getKey(item: any, index?: number): string { 

    return item.key; 

  } 

  private _onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => { 

    const { columns, items } = this.state; 

    const newColumns: IColumn[] = columns.slice(); 

    const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0]; 

    newColumns.forEach((newCol: IColumn) => { 

      if (newCol === currColumn) { 

        currColumn.isSortedDescending = !currColumn.isSortedDescending; 

        currColumn.isSorted = true; 

        this.setState({ 

          announcedMessage: `${currColumn.name} is sorted ${ 

            currColumn.isSortedDescending ? ’descending’ : ’ascending’ 

          }`, 

        }); 

      } else { 

        newCol.isSorted = false; 

        newCol.isSortedDescending = true; 

      } 

    }); 

    const newItems = _copyAndSort(items, currColumn.fieldName!, currColumn.isSortedDescending); 

    this.setState({ 

      columns: newColumns, 

      items: newItems, 

    }); 

  }; 

function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] { 

  const key = columnKey as keyof T; 

  return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1)); 

Update Index.Ts:

Update the index.ts file, declaring the container and calling the ActivitiesGrid component in updateView.

import {IInputs, IOutputs} from ”./generated/ManifestTypes”; 

import * as React from ’react’; 

import * as ReactDOM from ’react-dom’; 

import { ActivitiesGrid } from ’./DetailsList-Simple-sort’ ; 

 

export class RelatedActivitiesGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> { 

    private _container: any 

    constructor() 

    { 

    } 

    public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement) 

    { 

        this._container = container 

    } 

    public updateView(context: ComponentFramework.Context<IInputs>): void 

    { 

        let accountid=context.parameters.accountid?.raw ; 

        let appProps: any    

        appProps = { 

            accountid: accountid, 

            pcfContext:context 

        }; 

        ReactDOM.render(React.createElement(ActivitiesGrid, appProps), this._container); 

    } 

    public getOutputs(): IOutputs 

    { 

        return {}; 

    } 

    public destroy(): void 

    {      

    } 

Build The Solution And Deploy To CRM Org:

There are different ways to deploy the control to our instances; we can build a solution and manually import it.  I am mostly using it to create an auth profile and pushing it directly using the following commands.  Read more

Open a new Terminal window and run the following commands to import the control to my trial org. 

  • Build the project 

 npm run build 

  • Create your authentication profile using the command 
  • Run the following command after ensuring that you have a valid authentication profile created (I have used “msd” as my publisher prefix) 

You can view the commands below in a new terminal window. 

Configure The PCF Control In CRM:

After running the above commands, you will see a new solution in your org as following. 

After the control is in place, I have configured the account number field’s control on the account entity form, as shown in the below screenshot.

Result:

Finally, you will see the grid as following on account form with the following functionality,  

  • Activities are showing from the account records as well as from the related contacts. 
  • You can navigate to the record by double-clicking the row. 
  • You can navigate the regarding (Account/Contact) record by clicking the regarding cell. 
  • You can sort by clicking headers. 
  • The most remarkable thing is the look and feel, which is somehow like the OOB sub-grid.

Learn more about our Dynamics capabilities 

AlphaBOLD Your Partner in Cutting-Edge Software Development

Join forces with AlphaBOLD for cutting-edge software development. Let’s collaborate to create software that stands out.

Request a Consultation

Conclusion

This blog will help those who needed to roll up activities from any related entities for certain requirements. The Fluent UI Detail List component in this blog helps those unfamiliar with React & MS Fluent UI components with PCF Control. Feel free to reach out to us using the comment box below. You can also get in touch with our BOLDEnthusiasts via the contact us page. 

Explore Recent Blog Posts

Infographics show the 2021 MSUS Partner Award winner

Related Posts

Receive Updates on Youtube