Add comments to SharePoint list items using the REST API

One of the newest features to come to SharePoint Online is the ability to add comments within the activity panel on SharePoint list items. In a previous post, I talked about how we can retrieve these new comments using the SharePoint REST API.

List comments are a great new way for users to collaborate together on list items as users can have threaded conversations with one another.

In the past we would have used a multi-line text column with “append changes” to create threaded conversations on list items. The problem with this is this method creates new versions and those comments are difficult to retrieve programmatically. Please see my post on retrieving values in multi-line text fields more efficiently.

Add a comment using the REST API

Recently, I’ve been seeing more posts on support forums and in social asking how these comments can be accessed and created programmatically. Well, like most things in the modern interface, they are implemented using client-side code, which must mean there is a REST API backing the functionality.

You will need to execute a POST request to the following endpoint

Endpoint https://yourtenant.sharepoint.com/sites/ContosoGroup/_api/web/lists/GetByTitle(“<YourListName>&#8221;)/items(<ItemId>)/Comments()

Parameters – The parameter you will pass through the body is called “text”. Construct your JSON object as {“text”:”your sample comment”}

fetch("https://yourtenant.sharepoint.com/sites/ContosoGroup/_api/web/lists/GetByTitle('Test')/items(<ItemId>)/Comments()", {
"headers": {
"accept": "application/json;odata=verbose",
"content-type": "application/json;charset=UTF-8",
"x-requestdigest": "<yourRequestDigest""
},
"body": "{\"text\":\"Add a new comment\"}",
"method": "POST",
"mode": "cors",
"credentials": "include"
});

Using the above POST call, you can submit a new comment to the list item as the current user (there is no way to add list item comments on-behalf of another user). When you execute the request and update your x-requestdigest, and the appropriate values for the URI, your comment will be successfully added to the list item (see below).

I hope you find this post helpful and if you have any questions, please feel free to reach out!

SharePoint Online – Retrieve values of a multi-line text field with append changes in a single REST call

One of the most common questions I see in SharePoint support forums is how to retrieve the history of a multi-line text field with “append changes” in SharePoint. In most cases, the answer is to retrieve each previous version of a SharePoint item and iterate through them to construct all values in the multi-line text field.

The reasoning behind this is fairly obvious — each instance of a change in the multi-line text field is stored in a separate version of the SharePoint item.

Take for example the following Test list. This list contains a title field and a multi-line text field with “append changes”

If you open up the information panel for this item, you will see the multiline text field with the previous values. Notice how I have updated this item twice and added a value to the Multi field multiple times.

In order to see how SharePoint stores this data, check out the version history. The version history shows the two times I have edited the item, with the latest version being the most recent value in the multi-line text field.

Now you can see why you need to loop through versions. If you did a standard REST call using something like _api/web/lists/getbytitle(‘Test’)/items(1), the Multi column would only have the value of “My second comment in the multiline text field”.

Well, fear no more! There is in fact a way to retrieve the values in your list item without having to iterate over each previous version of the item.

Introducing RenderExtendedListFormData()

An unknown and undocumented REST endpoint exists in SharePoint Online called RenderExtendedListFormData() is the solution we need. You may be saying, “oh, we shouldn’t use undocumented REST endpoints”. Valid point, however, you should be aware that this endpoint is exactly what is used when rendering the information pane in Modern SharePoint via first-party.

Below you will see how to make this request into SharePoint. The RenderExtendedListFormData method is a POST call and takes 5 parameters:

itemId – The ID of your list item

formId – The form name (this is how MSFT determines what fields are returned in the request based on the form)

mode – I haven’t figured out what this means yet

options – I haven’t figured out what this means yet

cutoffVersion – Which is the latest version you want to pull. e.g, if you pass in 0, it will retrieve every version up until the first. If you pass in “2” it will put every version up until 2.0.

fetch("https://yourtenant.sharepoint.com/sites/contosoportal/_api/web/GetList(@v1)/RenderExtendedListFormData(itemId=1,formId='editform',mode='2',options=47,cutoffVersion=0)?@v1=%27%2Fsites%2Fcontosoportal%2FLists%2FTest%27", {
   "headers": {
     "accept": "application/json;odata=verbose",
     "accept-language": "en-US,en;q=0.9",
     "content-type": "application/json;odata=verbose",
     "sec-fetch-dest": "empty",
     "sec-fetch-mode": "cors",
     "sec-fetch-site": "same-origin",
     "x-requestdigest": "<yourRequestDigest>",
   },
   "referrer": "https://yourtenant.sharepoint.com/sites/contosoportal/Lists/Test/AllItems.aspx",
   "referrerPolicy": "strict-origin-when-cross-origin",
   "body": "",
   "method": "POST",
   "mode": "cors",
   "credentials": "include"
 });

The Response

The response from this call includes a lot of data as it’s used for the rendering of the edit form, but if you look closely you’ll notice the values for the multiline text field are stored in d.RenderExtendedListFormData.<ColumName>

The Multi (your column name) object contains all of the values in the multi-line text field based on the parameters of my REST call above. The response includes the text value, who wrote the value (email, title, id) as well as when the item was created.

HOW AWESOME IS THAT?! I retrieved all appended changes of my multi-line text field, using a single REST call.

Caveats + FYI

As mentioned previously, this is an undocumented REST endpoint. Even though this is used first-party by Microsoft, you should be aware this endpoint is subject to change and your solutions will have a dependency on that. Use at your own risk!

If you know what the other parameters (mode / option) mean in the REST call, I’d love to find out! Please message me if you know!

Creating hyperlinks to missing pages in modern SharePoint

If you ever used the classic Wiki pages in SharePoint, there was a really cool feature that allowed users to link to pages that did not exist yet. In SharePoint Wikis, you could link to another page on your site using the double bracket syntax. Example:

Please go to the [[Announcements]] page.

SharePoint would handle this by connecting the the Announcements page to the text within the brackets. The really cool thing about this is if a user clicked on a link to a page that didn’t exist it yet, SharePoint would ask them to create a new page! This is similar to how Wikipedia creates empty hyperlinks to pages that still need to be created.

A question came up in a SharePoint sub-reddit asking whether this works in Modern SharePoint. The answer is YES!

Back to basics

To see this feature in action, create a new page in your SharePoint site and add a text webpart to the page.

Let’s say at the end of this text, we would like to add a link to an existing page within this site. All we have to do is start typing “[[” and SharePoint will return all the pages we can link to.

 

If we wanted to link to the Singapore trip report page, we could then just click that page from the dropdown, and it will become a direct link to that page. Pretty cool right!?

Creating a new page from a link

Now that we have seen how using the “[[” allows users to link to existing pages, lets see how Microsoft brought forward the classic Wiki linking functionality to allow users to create hyperlinks to pages that do not exist yet.

To do so, start by typing “[[“, but ignore the dropdown values for existing pages. Continue typing in the title for a new page and close it with two double brackets “]]”. Make sure you press space key or click away from the “]]” or else the resulting link will be a 404.

Once this happens, SharePoint will automatically create a hyperlink for us.

If you look closely at the link, you’ll notice that the URL points to a new page (doesn’t exist), and it passes along the ?wikiTitle=Communication query string.

The wikiTitle query string parameter notifies SharePoint to not throw a 404 error, but instead initiate the new modern page creation flow. When a user clicks on this link, SharePoint will popup the page template selection screen!

Once a user selects a template, they will be brought to the new Communication page to continue editing and finish creating the page.

This page has also now been linked correctly to our previous page!

SharePoint JSON Formatting – “Name cannot begin with the ‘=’ character”

Have you ever tried to apply column formatting to your SharePoint list views using code? If so, it’s likely you have come across this error and unfortunately there isn’t much information in the documentation on how the JSON Formatter string should be formatted.  Take for example this scenario:

Updating a view format using PnP PowerShell

PnP PowerShell has functionality for setting properties of a view using the command Set-PnPView

Updating the formatting on your list is done by passing in a JSON string to your Set-PnPView command using the CustomFormatter value property.

Set-PnPView -List "MyList" -Identity "MyViewName" -Values @{CustomFormatter = @'my-json-formatted-string'@}

Example JSON

Below is a basic example of applying a background color to a row on a view when the DueDate is less than now (date time).

{"schema":"https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json","additionalRowClass": "=if([$DueDate] <= @now, 'sp-field-severity--severeWarning', '')"}

Applying this format using PnP PowerShell

To apply this JSON script, you would use the following command

Set-PnPView -List "MyList" -Identity "MyViewName" -Values @{CustomFormatter = @'
{"schema":"https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json","additionalRowClass": "=if([$DueDate] <= @now, 'sp-field-severity--severeWarning', '')"}
'@
}

Running this command, you will likely receive the following error: Set-PnPView : Name cannot begin with the ‘=’ character, hexadecimal value 0x3D.

The Fix:

The reason you are seeing this error is because in the JSON itself you need to encode some of the values if you are using operators. What I mean by that is, if you are using &&, or operators in formulas such as “>=” or “<=“, you need to use their encoded values instead. In our example, we were using [$DueDate] <= @now. In order to apply this to our view, we need to encode “<=” into  “&lt;=” and the formula will work.

Below is the following command with a working JSON formatter value.

Set-PnPView -List "MyList" -Identity "MyViewName" -Values @{CustomFormatter = @'
{"schema":"https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json","additionalRowClass": "=if([$DueDate] &lt;= @now, 'sp-field-severity--severeWarning', '')"}
'@
}

 

Hope this helps you!

Diagnosing changes with the Modern SharePoint page version history.

If you love SharePoint, then you already know about it’s rich document and information management capabilities. Version history is a feature of SharePoint that is extremely powerful if you need to look back in history at the changes of an item. This works great if you are working with list items only, as you can see how the fields have changed on a specific item overtime. For documents and pages, SharePoint version history is unable to show us exactly what has changed within those pages.

Turns out, in modern SharePoint we do have better version history which shows us what changes have been done to modern site pages! This is an extremely powerful tool to keep track of changes of your modern SharePoint site pages over time. 

Viewing Version History

In order to view the version history of your modern SharePoint site pages, navigate to the page in question. In my scenario, we’ll be looking at a Communication site home page which has a series of hero, news and events web parts on it.Homepage

Accessing the version history is easy. On the page in question, select the “Published” button next to the “Edit” button in the top right of the page. The page will re-render and load a slide out panel from the right hand side, showing the version history of the page.

HomeVersion

Notice how you can see a history of changes that were completed on the page. In my example, edits were made to News, Spacer, Text, Quicklinks, and Events between version v3.0 and v2.0. If I would like to dive into this a bit more, I can select “Highlight changes on the page” toggle from the top of the panel. When turning this one, new boxes will highlight around the web parts that were changed with a specific color.

HomeVersion2

Green means added, Yellow means edited and Red means deleted.
How awesome is that!? Do you think this is valuable? Any new features you’d love to see come to version history?

Rendering multi-value Choice fields vertically using JSON Column Formatting

Recently I was perusing a SharePoint forum post and a member asked if there was a way to change the visual representation of a multiple value choice field in SharePoint. My first thought was to use JSON Column Formatting.

The problem

By default, SharePoint renders a multiple value choice field as a single string in a row, and renders the HTML as a single value in a div.

HTMLChoiceField

One question, if you aren’t familiar with JSON Column Formatting is how would we render these items as new lines if they represented as a single value in the HTML. You’d probably first go and see if you could split on the commas “,”… but unfortunately column formatting does not support a split function.

Introducing ‘forEach’

One feature that column formatting does have is the forEach function. This is an optional property that allows an element to duplicate itself for each member of a multi-valued field. To loop through multi-value fields we’d use the following format

"iteratorName in @currentField" or "iteratorName in [$FieldName]"

Once you’ve implemented the forEach, you have access to each member you are looping through by using the iterator name. For example, if we loop through @currentField using the following formula: "iteratorName in @currentField" we can gain access to each record using [$iteratorName].

Putting it into action

Now that we know we can loop through multi-choice fields, all we need to do is come up with a JSON column formatter which creates each record on it’s own row. See the below JSON object.

We are using the forEach property to loop through each choice value in the currentField. For each record, we set the textContext equal to the choice record, and then we just style the div to be displayed block and 100%

{
  "$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
  "debugMode": true,
  "elmType": "div",
  "children": [
    {
      "elmType": "div",
      "style": {
        "display": "block",
        "width": "100%"
      },
      "forEach": "choice in @currentField",
      "txtContent": "[$choice]"
    }
  ]
}

The end result turns the original choice field, to be rendered like so:

ChoicesHTML

MultipleLineChoice

 

Finding all Delve Blogs in your tenant using Search

Recently, Microsoft has announced they are retiring Delve blogs. In doing so, Microsoft has also given us a schedule of important dates relating to the retirement.

  • Beginning December 18th, 2019, tenants will not have the ability to create new Delve Blogs
  • Beginning January 18th, 2020 the ability to create new posts in existing Delve blogs will be discontinued
  • Beginning April 17th, 2020, existing Delve blogs will be deleted and removed from Delve profiles

If your organization has been using Delve blogs, you are probably thinking “wow, I don’t have much time to migrate Delve blogs into communication sites“. That’s correct, it does feel pretty rushed. If you are looking into finding all of the blog sites in your tenant, here is a search query to help you out.

Search Query

* path:yourtenant/portals/personal* ContentType:"Story Page"'&selectproperties='Author,SPWebUrl'

Search via REST

https://yourtenant/_api/search/query?queryText=’* path:yourtenant/portals/personal* ContentType:”Story Page”‘&selectproperties=’Author,SPWebUrl’

Search Explained

The above search query is fairly simple. It will search everything (*) where the path starts with the Delve Blog locations (path:yourtenant/portals/personal*) where the Content Type is the content type used for Delve Blogs (Story Page).


Another method for finding all blogs being used in your tenant is by using the Modernization Scanner. This is a tool that was designed to help companies modernize their classic sites by scanning tenants looking for things like InfoPath usage, Classic Workflows and more.

Well, starting in version 2.7, it will include the ability to scan your tenant for Delve blogs, using the same search methods above.

 

 

Exploring Modern page templates in SharePoint Online with REST

A long awaited feature in modern SharePoint has finally hit targeted release! We saw the ability to create page templates during Ignite last year and I’m happy to say they have been released to SharePoint Online. This post is going to give an overview of page templates. Be aware that functionality may change as this feature is currently in targeted release.

Who can create page templates?

Page templates can be created by a site owner or a SharePoint administrator.

How to create a page template

Creating a page template is quite easy.  First, create a new site page in your modern site and configure the web parts and sections for your page. Before saving your page, you’ll see a new option  in the “Save as draft” menu called “Save as template”

Template.png

Where are templates stored?

When you create a new template,  the template is stored inside the Site Pages library in a folder called “Templates”

Tempalte2

What properties determine if it’s a template?

If you look  closely at the properties of any Site Page using an API, you’ll see an internal column that denotes specific flags on the current item called OData__SPSitePageFlags. This is a (Collection.EdmString) property and a template will include the value “Template”.

OData__SPSitePageFlags = “Template”

Promote a Site Page as a template via REST

As a developer,  I’m always interested in seeing how we can achieve native UX functionality using code.  There is a REST endpoint available to take an existing page and save it as a template.

The REST endpoint

https://yourtenant.sharepoint.com/_api/sitepages/pages(<id>)/SavePageAsTemplate

Parameters

Body: “{\”__metadata\”:{\”type\”:\”SP.Publishing.SitePage\”}}”

The REST endpoint allows a developer to pass in the ID of the Site Page list item and POST to /SavePageAsTemplate to promote the page as a template. This will create a copy of the Site page and place it inside the /Templates folder.

REST Call Example

fetch("https://yourtenant.sharepoint.com/_api/sitepages/pages(6)/SavePageAsTemplate", {
    "credentials": "include",
    "headers": {
        "accept": "application/json",
        "accept-language": "en-US,en;q=0.9",
        "cache-control": "max-age=0",
        "content-type": "application/json;odata=verbose;charset=utf-8",
        "if-match": "*",
        "odata-version": "3.0",
        "x-http-method": "POST",
        "x-requestdigest": "0x0BA2BEBD58CB0E0673101B350B558A96D98A236A3170597FA62AA5B4D47A52E210E801048FA58AAA6218204AB7E861D770A0924753A1E071DC6812240C359615,13 May 2019 15:00:48 -0000"
    },
    "body": "{\"__metadata\":{\"type\":\"SP.Publishing.SitePage\"}}",
    "method": "POST",
    "mode": "cors"
});

View all templates via REST

Now that we know how to create a page template from an existing page, let’s show how to find all available templates via REST.

The REST endpoint

https://yourtenant.sharepoint.com/_api/sitepages/pages/templates?asjson=1

Parameters

asjson: 1

The REST endpoint accepts a GET request to return all templates in your site from the Site Pages library

REST Call Example

fetch("https://yourtenant.sharepoint.com/_api/sitepages/pages/templates?asjson=1", {
    "credentials": "include",
    "headers": {
        "accept": "application/json;odata.metadata=minimal",
        "accept-language": "en-US,en;q=0.9",
        "if-modified-since": "Mon, 13 May 2019 15:06:59 GMT",
        "odata-version": "4.0"
    },
    "body": null,
    "method": "GET",
    "mode": "cors"
});

 

REST Call Response

The REST call returns an array of SP.Publishing.SitePageMetadata objects

{
  "@odata.context":"https://yourtenant.sharepoint.com/_api/$metadata#SitePageMetadatas",
  "value":[{
    "@odata.type":"#SP.Publishing.SitePageMetadata",
    "@odata.id": "https://yourtenant.sharepoint.com/_api/SP.Publishing.SitePageMetadatac155ab55-017f-4594-9880-e73ff6b0f06e",   
    "@odata.editLink": "SP.Publishing.SitePageMetadatac155ab55-017f-4594-9880-e73ff6b0f06e",
    "AbsoluteUrl": "https://yourtenant.sharepoint.com/SitePages/Templates/Test(1).aspx",
    "AuthorByline": ["i:0#.f|membership|beau@yourtenant.onmicrosoft.com"],
    "BannerImageUrl": "https://yourtenant.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
     "BannerThumbnailUrl": "",
        "ContentTypeId": null,
        "Description": "How do you get started? Tests Select 'Edit' to start working with this basic two-column template with an emphasis on text and examples of text formatting. With your page in edit mode, select this paragraph and replace it with your own text. Then, se\u2026",
        "DoesUserHaveEditPermission": true,
        "FileName": "Test(1).aspx",
        "FirstPublished": "0001-01-01T00:00:00-08:00",
        "Id": 8,
        "IsPageCheckedOutToCurrentUser": true,
        "IsWebWelcomePage": false,
        "Modified": "2019-05-13T15:11:52Z",
        "PageLayoutType": "Article",
        "Path": {
            "DecodedUrl": "SitePages/Templates/Test(1).aspx"
        },
        "PromotedState": 0,
        "Title": "Test",
        "TopicHeader": "TEXT ABOVE TITLE",
        "UniqueId": "0ef8700a-07de-4a44-8793-3a38a8e3189d",
        "Url": "SitePages/Templates/Test(1).aspx",
        "Version": "1.0",
        "VersionInfo": {
            "LastVersionCreated": "0001-01-01T00:00:00-08:00",
            "LastVersionCreatedBy": ""
        }
}]
}

 

Let me know if you have any interesting ideas for how to incorporate these page templates into your custom solutions!

Experiment – Find out where SPFx Web Parts are being used in Modern SharePoint sites

Recently, I saw a post on Tech Community that asked if there were APIs available to find out where a specific web part may be used in an environment. The reason was to provide a list of sites so an email could be sent to those specific site owners to let them know that a deployment was going to happen for some SPFx web parts.

My first thought was to loop through all of the sites and find out if the the SharePoint Framework app had been installed. This would work except in the case of a tenant wide deployment of the SharePoint Framework web parts. So instead of finding out where a web part has been installed, we need to find out where a web part was actually being used.

The Experiment

To me, this sounded like a great idea, unfortunately, I wasn’t aware of any APIs that were able to do this. Then I got to thinking, maybe we could use the search API to do this. In those post, I am going to try and see if we can use the Search API to find web part usage in SharePoint. Be aware this solution would only work for modern pages using your SPFx web part.

CanvasContent1

When adding web parts to a modern site page in Office 365, the HTML content is saved into a column called “CanvasContent1”. I quickly looked at the search schema to see if I would be able to search on this column.

canvascontent

Unfortunately, by default, the CanvasContent1 managed property called CanvasContent1OWSHTML didn’t have a crawled property mapped to it.  So I decided to map ows_CanvasContent1 to a new RefinableString.

untitled

Now, in order for the column to be searchable we have to wait for the column to re-crawl in our environment… this could take some time in SharePoint Online.

What to search for

The CanvasContent1 field contains a bunch of html data about the contents of the page. This includes web parts and their configuration including properties. Stored inside the CanvasContent1 field will also include the ID of the web parts configured on the page.With this in mind, I figured it would be fairly easy to find where a web part is being used in an environment by searching against this field.

Let’s say that I have a web part with the component id of 62799350-83b2-40a1-b35d-5417cc54daea as shown in this SPFx manifest file.

untitled

If I execute a search query against the RefinableString field where it contains this GUID, I should be fairly certain the page contains my web part.

Using the SharePoint Search  REST API, I can execute the following call to return the Title, Path and Site of the page that is rendering my web part.

Request

https://testsite.sharepoint.com/sites/test/_api/search/query?QueryText='RefinableString134:62799350-83b2-40a1-b35d-5417cc54daea*'&selectProperties='Title,Path,SPWebUrl'

Response

In the response, I have found 2 pages where my web part is being loaded.

<d:query xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml"m:type="Microsoft.Office.Server.Search.REST.SearchResult">
<d:ElapsedTime m:type="Edm.Int32">104</d:ElapsedTime>
<d:PrimaryQueryResult m:type="Microsoft.Office.Server.Search.REST.QueryResult">
<d:CustomResults m:type="Collection(Microsoft.Office.Server.Search.REST.CustomResult)"/>
<d:QueryId>37de1142-2baf-4ea6-ab0b-b646a0a57d31</d:QueryId>
<d:QueryRuleId m:type="Edm.Guid">00000000-0000-0000-0000-000000000000</d:QueryRuleId>
<d:RefinementResults m:null="true"/>
<d:RelevantResults m:type="Microsoft.Office.Server.Search.REST.RelevantResults">
<d:GroupTemplateId m:null="true"/>
<d:ItemTemplateId m:null="true"/>
<d:Properties m:type="Collection(SP.KeyValue)">
<d:element>
<d:Key>GenerationId</d:Key>
<d:Value>9223372036854775806</d:Value>
<d:ValueType>Edm.Int64</d:ValueType>
</d:element>
<d:element>
<d:Key>indexSystem</d:Key>
<d:Value/>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element>
<d:Key>ExecutionTimeMs</d:Key>
<d:Value>47</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element>
<d:Key>QueryModification</d:Key>
<d:Value>
RefinableString134:62799350-83b2-40a1-b35d-5417cc54daea* -ContentClass=urn:content-class:SPSPeople
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element>
<d:Key>RenderTemplateId</d:Key>
<d:Value>
~sitecollection/_catalogs/masterpage/Display Templates/Search/Group_Default.js
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element>
<d:Key>StartRecord</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element>
<d:Key>IsLastBlockInSubstrate</d:Key>
<d:Value>true</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsFirstBlockInSubstrate</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsFirstPinnedResultBlock</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsLastPinnedResultBlock</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsFirstRankedResultBlock</d:Key>
<d:Value>true</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsLastRankedResultBlock</d:Key>
<d:Value>true</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>MixedTableOrder</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
</d:Properties>
<d:ResultTitle m:null="true"/>
<d:ResultTitleUrl m:null="true"/>
<d:RowCount m:type="Edm.Int32">2</d:RowCount>
<d:Table m:type="SP.SimpleDataTable">
<d:Rows>
<d:element m:type="SP.SimpleDataRow">
<d:Cells>
<d:element m:type="SP.KeyValue">
<d:Key>Rank</d:Key>
<d:Value>16.8176937103271</d:Value>
<d:ValueType>Edm.Double</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>DocId</d:Key>
<d:Value>17601926184608</d:Value>
<d:ValueType>Edm.Int64</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Title</d:Key>
<d:Value>Home</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Path</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test/SitePages/AlertnativeHome.aspx
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>SPWebUrl</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>OriginalPath</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test/SitePages/AlertnativeHome.aspx
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>PartitionId</d:Key>
<d:Value>ca45b536-df01-44b6-afa0-d3f8e7ebb312</d:Value>
<d:ValueType>Edm.Guid</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>UrlZone</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Culture</d:Key>
<d:Value>en-US</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>ResultTypeId</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>RenderTemplateId</d:Key>
<d:Value>
~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Default.js
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
</d:Cells>
</d:element>
<d:element m:type="SP.SimpleDataRow">
<d:Cells>
<d:element m:type="SP.KeyValue">
<d:Key>Rank</d:Key>
<d:Value>16.8176937103271</d:Value>
<d:ValueType>Edm.Double</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>DocId</d:Key>
<d:Value>17601957874490</d:Value>
<d:ValueType>Edm.Int64</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Title</d:Key>
<d:Value>Home</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Path</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test/SitePages/AlternativeHome2.aspx
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>SPWebUrl</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>OriginalPath</d:Key>
<d:Value>
https://testsite.sharepoint.com/sites/test/SitePages/AlternativeHome2.aspx
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>PartitionId</d:Key>
<d:Value>ca45b536-df01-44b6-afa0-d3f8e7ebb312</d:Value>
<d:ValueType>Edm.Guid</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>UrlZone</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>Culture</d:Key>
<d:Value>en-US</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>ResultTypeId</d:Key>
<d:Value>0</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element m:type="SP.KeyValue">
<d:Key>RenderTemplateId</d:Key>
<d:Value>
~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Default.js
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
</d:Cells>
</d:element>
</d:Rows>
</d:Table>
<d:TotalRows m:type="Edm.Int32">2</d:TotalRows>
<d:TotalRowsIncludingDuplicates m:type="Edm.Int32">2</d:TotalRowsIncludingDuplicates>
</d:RelevantResults>
<d:SpecialTermResults m:null="true"/>
</d:PrimaryQueryResult>
<d:Properties m:type="Collection(SP.KeyValue)">
<d:element>
<d:Key>RowLimit</d:Key>
<d:Value>500</d:Value>
<d:ValueType>Edm.Int32</d:ValueType>
</d:element>
<d:element>
<d:Key>SourceId</d:Key>
<d:Value>8413cd39-2156-4e00-b54d-11efd9abdb89</d:Value>
<d:ValueType>Edm.Guid</d:ValueType>
</d:element>
<d:element>
<d:Key>CorrelationId</d:Key>
<d:Value>02b6b69e-10c1-7000-727b-bc8a6f5546b9</d:Value>
<d:ValueType>Edm.Guid</d:ValueType>
</d:element>
<d:element>
<d:Key>WasGroupRestricted</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsPartial</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>HasParseException</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>WordBreakerLanguage</d:Key>
<d:Value>en</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element>
<d:Key>IsPartialUpnDocIdMapping</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>EnableInterleaving</d:Key>
<d:Value>true</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>IsMissingUnifiedGroups</d:Key>
<d:Value>false</d:Value>
<d:ValueType>Edm.Boolean</d:ValueType>
</d:element>
<d:element>
<d:Key>Constellation</d:Key>
<d:Value>iC6B8D</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
<d:element>
<d:Key>SerializedQuery</d:Key>
<d:Value>
<Query Culture="en-US" EnableStemming="True" EnablePhonetic="False" EnableNicknames="False" IgnoreAllNoiseQuery="True" SummaryLength="180" MaxSnippetLength="180" DesiredSnippetLength="90" KeywordInclusion="0" QueryText="RefinableString134:62799350-83b2-40a1-b35d-5417cc54daea*" QueryTemplate="" TrimDuplicates="True" Site="532bd9a4-869f-45e2-b5f0-323f6604a429" Web="c67564a0-242f-40bf-b4ac-a0ea8dbc935e" KeywordType="True" HiddenConstraints="" />
</d:Value>
<d:ValueType>Edm.String</d:ValueType>
</d:element>
</d:Properties>
<d:SecondaryQueryResults m:type="Collection(Microsoft.Office.Server.Search.REST.QueryResult)"/>
<d:SpellingSuggestion/>
<d:TriggeredRules m:type="Collection(Edm.Guid)"/>
</d:query>

In my result set, I have found two pages in the same site that was using this specific web part. With a larger result set that comes back, I could loop through the records and figure out which sites are running my web part, and send an email accordingly to each site owner about the upcoming web part deployment!

Final Thoughts

This was a fun experiment to test out and I am going to continue to do some more exploration to see if there are other methods possible for finding out this information. I haven’t been able to do thorough testing, but I would love to hear if this works for you and/or if you have another approach!

Caveats/Feedback

  1. This would only work for web parts placed on modern pages
  2. If site owners have disabled content from being searchable, it will not show up. (Thanks Paul Bullock!)
  3. There are much better approaches to monitoring this type of information. As Sam Crewdson noted on twitter  the use of Application Insights is a robust way to track and monitor solutions in your environment. Here is a blog post by Chris O’Brien showing how to use application insights with SPFx.

Get site script from list using REST

New updates from Microsoft for site scripts and site designs really improved the creation of the JSON required in a site script. One of the newest features is the ability to generate a site script from an existing list. If you have seen my previous post on creating managed metadata fields using site scripts, you’d quickly learn how tedious and complex JSON can be.

Thankfully, there is a new process that allows you to generate the site script syntax from an existing list in your SharePoint environment, making it much easier to construct these site scripts. The current documented method for doing this is by using a PowerShell command called Get-SPOSiteScriptFromList.

There is another method for achieving this and it’s by using the REST api. Currently, this endpoint is not documented,  but I have submitted a PR in the documentation to correct this.

GetSiteScriptFromList

GetSiteScriptFromList is a new endpoint that allows you to generate the syntax required for a site script from an already created list.

Request

ParametersPass in a listUrl parameter with the url to the list you want to create the site script syntax for.

fetch("https://testsite.sharepoint.com/sites/test/_api/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.GetSiteScriptFromList", {
    "credentials": "include",
    "headers": {
        "accept": "application/json;odata=verbose",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/json;odata=verbose",
        "x-requestdigest": "YourRequestDigest"
    },
    "referrer": "https://testsite.sharepoint.com/sites/test",
    "referrerPolicy": "no-referrer-when-downgrade",
    "body": "{\"listUrl\":\"https://testsite.sharepoint.com/sites/test/Lists/Contoso%20customer%20list\"}",
    "method": "POST",
    "mode": "cors"
});

Response

The response contains an array of actions for creating this list.  The command supports most field types, including complex field types such as managed metadata columns.

{"d":{"GetSiteScriptFromList":"{
  "actions": [
    {
      "verb": "createSPList",
      "listName": "Contoso customer list",
      "templateType": 100,
      "subactions": [
        {
          "verb": "addSPView",
          "name": "All Items",
          "viewFields": [
            "LinkTitle"
          ],
          "query": "",
          "rowLimit": 30,
          "isPaged": true,
          "makeDefault": true
        }
      ]
    },
    {
      "verb": "addNavLink",
      "url": "Lists/Contoso customer list/AllItems.aspx",
      "displayName": "Contoso customer list",
      "isWebRelative": true
    }
  ]
}"}}

Now what?

Now that we have the syntax to create the list, we can just add the values from the actions array to a new or existing site script. For information on how to create a site script, follow the documentation here.