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

Objective  

We can show activities from contacts on the parent account form in the timeline and associated grids via the roll-up view. But we had a requirement to show related contact activities on the account form in a section/sub-grid. In this blog post, we will go through the steps for creating a PCF control to show the activities on account form from the related contacts. Also, we will know the way to use the MS Fluent UI and React with PCF Control. 

  • Showing activities in a sub-grid on account form from related contacts 
  • Microsoft Fluent UI, Detail list in PCF control 
  • React use in PCF Control 

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.

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[“_regardingob[email protected]”], 

      regardingobjectid_LookupLogicalName:e[“_regardingo[email protected]”], 

 

      activitytypecode_Value:e.activitytypecode, 

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

       

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

 

      ownerid_Guid:e[“_ownerid_value”],      

      ownerid:e[“_o[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

 

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. 

 

Posted in: CRM