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   

Learn more about our Dynamics capabilities

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.

image001

PCF Control Project hierarchy  

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

image003

Learn more about our Dynamics capabilities

Open the PCF Project in Visual Studio Code

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

image005

 

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> 

  <controlnamespace="xRM.Demo.PCFControls"constructor="RelatedActivitiesGrid"version="0.0.1"display-name-key="RelatedActivitiesGrid"description-key="RelatedActivitiesGrid description"control-type="standard"> 

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

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

   <resources> 

      <codepath="index.ts"order="1"/>     

    </resources>   

    <feature-usage>    

      <uses-featurename="Utility"required="true"/> 

      <uses-featurename="WebAPI"required="true"/> 

    </feature-usage>     

  </control> 

</manifest> 

Add a new file DetailsList-Simple.tsx under the RelatedActivitiesGrid folder.

image007

Add following code to DetailList-Simple.tsx file.

import*asReactfrom'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'; 

importStackfrom'office-ui-fabric-react/lib/components/Stack/Stack'; 

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

 

initializeIcons(); 

 

exportinterfaceIDetailsListState { 

  columns: IColumn[]; 

  items: IDetailsListItem[];  

  announcedMessage?: string;   

} 

 

exportinterfaceIDetailsListItem { 

  key: string; 

  subject: string; 

  regardingobjectid:string; 

  activitytypecode: string; 

  statecode: string; 

  ownerid:string;  

  scheduledend: string; 

  createdon: string; 

  modifiedon: string; 

} 

exportclassActivitiesGridextendsReact.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; 

    constcolumns: IColumn[] = [       

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

        onRender:item=> (           

          <Linkkey={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=> (           

          <Linkkey={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=> (           

          <Linkkey={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 

    }; 

  } 

   

  publicrender(): JSX.Element { 

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

    return (   

      <Fabric> 

        <divstyle={{ 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 && ( 

              <StackhorizontalAlign='center'> 

              <Text>{announcedMessage}</Text> 

            </Stack> 

              )} 

          </div>        

      </Fabric>    

    ); 

  } 

 

  publiccomponentDidUpdate(previousProps: any, previousState: IDetailsListState) { 

    

  } 

 

  privatepopulateRecords(results:any): any{  

    let_allItems: any[]=[]; 

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

      lete=results.entities[i]; 

      _allItems.push({ 

      key:e.activityid,  

      subject:e.subject,      

      regardingobjectid_Guid:e["_regardingobjectid_value"],      

      regardingobjectid:e["[email protected]ue"], 

      regardingobjectid_LookupLogicalName:e["[email protected]e"], 

 

      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?newDate(e.scheduledstart).toLocaleDateString():""),    

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

       

      scheduledend:( e.scheduledend!=null?newDate(e.scheduledend).toLocaleDateString():""),  

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

     

      createdon: ( e.createdon!=null?newDate(e.createdon).toLocaleDateString():""),  

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

     

      modifiedon: ( e.modifiedon!=null?newDate(e.modifiedon).toLocaleDateString():""), 

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

      });   

    } 

    return_allItems; 

  } 

 

  private_getKey(item: any, index?: number): string { 

    returnitem.key; 

  } 

 

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

    const { columns, items } = this.state; 

    constnewColumns: IColumn[] = columns.slice(); 

    constcurrColumn: 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; 

      } 

    }); 

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

    this.setState({ 

      columns:newColumns, 

      items:newItems, 

    }); 

  }; 

} 

 

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

  constkey = columnKeyaskeyofT; 

  returnitems.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*asReactfrom'react'; 

import*asReactDOMfrom'react-dom'; 

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

 

exportclassRelatedActivitiesGridimplementsComponentFramework.StandardControl<IInputs, IOutputs> { 

    private_container: any 

    constructor() 

    { 

 

    } 

 

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

    { 

        this._container = container 

    } 

     

    publicupdateView(context: ComponentFramework.Context<IInputs>): void 

    { 

        letaccountid=context.parameters.accountid?.raw ; 

        letappProps: any    

        appProps = { 

            accountid:accountid, 

            pcfContext:context 

        }; 

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

    } 

 

    publicgetOutputs(): IOutputs 

    { 

        return {}; 

    } 

     

    publicdestroy(): 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 

         pac auth create --url https://{yourorg}.crm.dynamics.com

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

      pac pcf push --publisher-prefix msd

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

image009

Configure the PCF Control in CRM   

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

image011

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. 

image013

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
 

image015

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

Leave a Reply

Your email address will not be published.