Externally Sharing – SPFX IsExternallyShared Field Customizer


This is my last post of the Externally Sharing series.

This post will show you how I have created a SPFX field customizer, where if it is displaying in the view, it will perform a call to the GetSharingInformation, then iterate through the links -> Invitations and principals results looking for external users. If it finds an external user, it displays a sharing link for the item, or it doesn’t.

 

ExternalShare2

You can find my Git Hub project at the following URL: https://github.com/pmatthews05/spfx-IsExternallyShared

The field customizer is a REACT SPFX project. As the control onInit() it grabs the list id from the pageContext.


@override
  public onInit(): Promise {
    // Add your custom initialization to this method.  The framework will wait
    // for the returned promise to resolve before firing any BaseFieldCustomizer events.
    Log.info(LOG_SOURCE, 'Activated IsExternallySharedFieldCustomizer with properties:');
    Log.info(LOG_SOURCE, `The following string should be equal: "IsExternallySharedFieldCustomizer" and "${strings.Title}"`);
    this.listId = this.context.pageContext.list.id;
    return Promise.resolve();
  }

As the control onRenderCell event fires, I grab the listitem’s ID. Then pass the ListID and the ID of the item to my .tsx control.

@override
  public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
    const id = event.listItem.getValueByName('ID');
    Log.info(LOG_SOURCE, `Loaded onRenderCell: ID: ${id}`);
    const isExternallyShared: React.ReactElement =
      React.createElement(IsExternallyShared,
        {
          id: id,
          listId: this.listId,
          context: this.context
        } as IIsExternallySharedProps);
    ReactDOM.render(isExternallyShared, event.domElement);
  }

The ExternallyShared.tsx file sets its externalShared state to 0. There are 3 values for externalShared.

0 – Loading

1 – Shared

2- Not Shared

The ExternallyShared.tsx file calls the GetSharingInformation/permissionsInformation in a POST REST API call. From the results I get back (assuming successful), I loop through the links, and principals looking for external users. As soon as I find 1 external user I return my results back. The code here isn’t perfect. I should really create an Interface for the returning json results.

export default class IsExternallyShared extends React.Component {
  constructor(props: IIsExternallySharedProps, state: IIsExternallySharedState) {
    super(props, state);
    this.state = {
      externallyShared: 0
    };
  }
  @override
  public componentDidMount(): void {
    Log.info(LOG_SOURCE, 'React Element: IsExternallyShared mounted');
    this._getIfExternallyShared()
      .then(isExternallyShared => {
        this.setState({
          externallyShared: isExternallyShared
        });
      })
      .catch(error => {
        Log.error(LOG_SOURCE, error);
      });
  }
  @override
  public componentWillUnmount(): void {
    Log.info(LOG_SOURCE, 'React Element: IsExternallyShared unmounted');
  }

 @override
 public render():React.ReactElement {
 return (

 

);
}
privateasync_getIfExternallyShared():Promise {
 var response = await this.props.context.spHttpClient.post(`${this.props.context.pageContext.web.absoluteUrl}/_api/web/Lists('${this.props.listId}')/GetItemById('${this.props.id}')/GetSharingInformation/permissionsInformation`, SPHttpClient.configurations.v1, null);
if (response.status!=200) {
 return 0;
}
  var jsonReponse = await response.json();
    let returnValue = 2;
    for (let link of jsonReponse.links) {
      if (link.linkDetails.HasExternalGuestInvitees) {
        returnValue = 1;
        break;
      }
    }

    if(returnValue == 2)
    {
      for(let principal of jsonReponse.principals)
      {
        if(principal.principal.isExternal){
          returnValue =1;
          break;
        }
      }
    }

    return returnValue;
  }

As you can see in the render() method, I have created my own ExternallySharedIcon control. Passing in the externallyShared value.

  

The ExternallySharedIcon.tsx control just displays an 64based image depending on the value.

  1. – Shows the spinning loading icon
  2. – Shows the sharing icon
export default class ExternallySharedIcon extends React.Component{
    private SHARED_ICON: string = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAG00AABtNAWneuvsAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAACkUlEQVRoQ9XaO2gVQRTG8ev7VSiKASGi2PgANYJaxE5IYqdiZavYWAk2IgoWgp1YaaxEG0HUSiy1sooGQnyAoDYqgiGg+ADf/08YGMLJvTP7mNn94FeES3ZPcmdnz8xupyEZwCVM4At+4j3u4QiWoJFZhZv428M7HECjsgGvYRVs+YMzaESW4yWsQns5huy5Cqu4EF+xDtmik/+CVVyoy8iWU7CKijGNecgSTY1WUbF2IEuewiooVrZp9RmsgmIdQtLMxWFo/FoFxRpEsuzFY1iFFPEdSdqLbbgPq4gybqPWrMU1/IZVQBk65k5EZQH6sOL/T7NHn1/AN1gnr8JFBEUFn8U41N66A3zELezDHCiLcAJT8E9WtTuYj65RUcfxGdZBfA+h5iqmq3T0OzpPyLT6A+fR8+6r4tVnWAepiobXObhZRMPzKB5hZo/0AVewEUE5Df8AVbuL9Zgty7AFahP64YZoUDZDX5V14rKeYwi15gask5fxCSehYVJrFkMLBauIIjRXX8caJMluWIUU8QRJ+xTlIKxiYr2CmrbkqfIPyLJS2gWroCJ0596DpKn6ItYejma1ZBexolnDKqYMN40uRO2p80b2AsOoPa1uJZSUzdxSKBpevZq5TYiK2lyNX/9glgdQO63p0/q8mzfQedQnWZ/7tB4Jaqf9rIa1oNGiRQuaEbivWP/FxixorMQuKauckmcKXlKWiS5ALerLbuJa1CjqxpskW1HHtoqGUtJoY2sMVjFFJNvY8qMLvrVbi36q2p1OvrnrMgmroFjZttdb/4Cj9Y+Y9JDPv7MXkfUhnzIKq7AQ2R+zKq1/0K3oVYOYLrZRrxq4rIRe9lBxVtHOW+xHY7Md3V630aaDkU7nH6/PvLG2FyRSAAAAAElFTkSuQmCC";
    private LOADING_ICON: string = "data:image/gif;base64,R0lGODlhIAAgAJEDAMbvwFTOQ37acf///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgADACwAAAAAIAAgAAACUpyPqcvtD6Oc1AAhALq5HhEEAgKKnlGOX6h6nHa850y7GCzJS0rxO9sDLnS5W+2ITCopxEiTJJT4FNNIdWOcPJdcWyeW9VSvk3G00tx21+y2owAAIfkECQoAAwAsAAAAACAAIAAAAk6cj6nL7Q+jnNQAIUB9IgSxOd0XNldWpup6alPLjOAkx94s1Qv8Yu4KDAqHJh+Fp9DlbjYSjbkz9lDEagk5wFKUA+4WavBOf1mp9YxOOwoAIfkECQoAAwAsAAAAACAAIAAAAkecj6nL7Q+jnNQAIUB9IgSxOd0XNldWpup6aisygu8Rz0dr5/rOU/j0U9QmQ6FHJikmghFm7/lgOolHWDWkVPowLgsXCg6LCwAh+QQJCgADACwAAAAAIAAgAAACQZyPqcvtD6Oc1AAhQH0iBLE53Rc2V1am6npqKzKC7xHPR2vn+s5TeF4DemS2X++INA6UlKDBWYFCfRiXpYrMarcFACH5BAkKAAMALAAAAAAgACAAAAI7nI+py+0Po5zUACFAfSIEsTndFzZXVqbqemorMoLvEc9Ha+f6zlN4XgN6ZLZf74g0zoI25kyJjEqniAIAIfkECQoAAwAsAAAAACAAIAAAAjecj6nL7Q+jnNQAIUB9IgSxOd0XNldWpup6aisygu8Rz0dr5/rOU3heA3pktl/viEwql8ymM1cAACH5BAkKAAMALAAAAAAgACAAAAIynI+py+0Po5zUACFAfSIEsTndFzZXVqbqemorMoLvEc9Ha+f6zvf+DwwKh8Si8YhUFQAAIfkECQoAAwAsAAAAACAAIAAAAiqcj6nL7Q+jnNQAIUB9IgSxOd0XNldWpurKtu4Lx/JM1/aN5/rO9/4vKwAAIfkEBQoAAwAsAAAAACAAIAAAAiqcj6nL7Q+jnNQBIUB9IgSxOd0XNldWpurKtu4Lx/JM1/aN5/rO9/6vKgAAIfkEBQoAAwAsGAAKAAQABAAAAgeEZBGneFAAACH5BAUKAAMALBgAEgAEAAQAAAIHhGQRp3hQAAAh+QQFCgADACwSABgABAAEAAACB4RkEad4UAAAIfkEBQoAAwAsCgAYAAQABAAAAgeEZBGneFAAACH5BAUKAAMALAQAEgAEAAQAAAIHhGQRp3hQAAAh+QQFCgADACwEAAoABAAEAAACB4RkEad4UAAAOw==";
    @override
    public render(): React.ReactElement {
        const image = this._getSharedStatus(this.props.isExternallyShared);
        if (image != "") {
            return (
                <img src="{image}" width="17px" />
            );
        }
        else {
            return ();
        }
    }
    private _getSharedStatus(value: number): string {
        if (value == 0)
            return this.LOADING_ICON;
        if (value == 1)
            return this.SHARED_ICON;
        if (value == 2)
            return "";
    }
}

The github project shows you how to create the column for the list. It is a standard text column, that doesn’t show up in any New/Edit forms. It only shows up in the view field. I’m using a pnp template to add it to the site collection.

Taking it further

The spfx-isExternallayShared field customizer is a very simple version of the one I created for my client. I was concerned with the number of times I was calling the GetSharingInformation, especially on very large libraries with many users. The call is only made for the number of items that are currently displayed on the screen to the user, however, when you start multiplying that by a possible 2000+ users that is a much larger load on SharePoint. Therefore, I moved the calling of the REST API to an Azure Function. Using the uniqueID of the item I was able to also use Redis Cache.

The first person who visited the library for each row would perform the actual GetSharingInformation call within the Azure Function, as there is nothing in the Redis Cache, then store either a 0 (not externally shared) or 1 (externally shared) with the UniqueID of the item as the key in Redis Cache. The second call to the library, the Azure Function would first check the cache, and just return that value.

The downside was the additional cost of running Azure Functions and Redis Cache, and that the externally shared value was no longer up to date and could be an hour out of date.

UPDATE: It turns out moving it to an Azure Function, where the calls are being made by one App account causes throttling issues. The client had over 79k calls made in an hour (many document libraries and users), and the App token that I was using got blocked by Microsoft. After further conversations with technical people at Microsoft, the calls need to be made in the context of the user. Since I’ve implemented the simple solution above, we haven’t noticed any throttling issues. Therefore if you want to move to an Azure Function, any calls back into SharePoint should really be done in the context of the user.

Advertisements

SPFX obtaining the URL Query Parameters


This is a very quick blog post, but I needed to obtain the URL parameters of the page for an SPFX webpart.

There is a built in method for obtaining URL parameters.

import { UrlQueryParameterCollection } from '@microsoft/sp-core-library';

...

var queryParms = new UrlQueryParameterCollection(window.location.href);
var myParm = queryParms.getValue("myParam");

I have found this very useful with Console Logs. I have a method called “logMessageToConsole” passing in the message. If I find a parameter called debug in my query string, then all the console logs are written out.

private logMessageToConsole(message:string)
{
var queryParms=new UrlQueryParameterCollection(window.location.href);
  if(queryParms.getValue("debug")){
    console.log(message);
 }
}

That way I only need to add ?debug=1 to the end of my URL or &debug=1 if there are already querystring parameters, and then I can see all my console messages. This is useful to quickly debug on production environments.