Monday, August 6, 2018

Highlighting which page is the welcome page in a site

A week ago Joanne Klein posted a request on twitter where she asked if it was possible to easily see which page is the home page or welcome page of a site. In a library with hundreds of pages, this can be quite useful indeed.

image

Technically the information about which page is the home page is stored as property on the root folder of the web (almost a mouthful there), which translates to that it’s not stored on the page it self, thus not “easily” set by adding a column.

The developer in me figured I could whip up a quick custom field renderer for the modern page library and have this working in a matter of minutes. Turns out, it’s not that easy. (A field renderer is a piece of code which shows a column value in any way you decide, instead of the default way – for example add a red background color.)

Either way, head over to https://github.com/Puzzlepart/spfx-solutions/tree/master/Pzl.Ext.HighlightHome if you want to download and install a solution which does highlight the home page for you. Install instructions are on that page as well.

image

Friday, July 27, 2018

Getting a sticky header for your modern SharePoint lists and libraries

image
Photo by Carson Arias on Unsplash.

If you have a list or library with many columns and items it becomes hard to navigate list when the header row scrolls out of view. Basically you are loosing context. Excel has had the ability to freeze the top header row for forever, and having this in modern lists and libraries would be extremely useful.

There is a user voice request for this at https://sharepoint.uservoice.com/forums/329214-sites-and-collaboration/suggestions/16077640-freeze-column-headers which you can upvote and maybe we will get it sooner rather than later.

Or………

Wednesday, July 25, 2018

Using Microsoft Graph to get a PDF preview of a file in SharePoint by file path

The viewfinder of a camera shows a photo of the sunset.
Photo by Glenn Carstens-Peters on Unsplash

There are multiple ways to get a PDF version of a file, so I figured I’d show how you via a path to a file in SharePoint can use the Microsoft Graph API to get a PDF version of that file. I’ll be using the Graph drive item conversion API for this.

A sample URL could look something like this: https://contoso.sharepoint.com/sites/asite/FooLib/lala/Document.docx

[Update]

After posting the question on Stack Overflow I received an answer from Vadim Gremyachev which takes it down to one API call.

Basically he clued me onto how you can create a sharing token for the item URL which is actually the file id. Code for this is listed in the Graph Sharing API docs.

First you base64 encode the URL, replace some characters and prefix with u!, then access the files via the /sharing API. The below code is using PowerShell to construct the token.

$url = 'https://contoso.sharepoint.com/sites/asite/FooLib/lala/Document.docx'
"u!"+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($url)).TrimEnd('=').Replace('/','_').Replace('+','-')

u!aHR0cHM6Ly9jb250b3NvLnNoYXJlcG9pbnQuY29tL3NpdGVzL2FzaXRlL0Zvb0xpYi9sYWxhL0RvY3VtZW50LmRvY3g

Armed with the token the result API call is:

https://graph.microsoft.com/v1.0/shares/u!aHR0cHM6Ly9jb250b3NvLnNoYXJlcG9pbnQuY29tL3NpdGVzL2FzaXRlL0Zvb0xpYi9sYWxhL0RvY3VtZW50LmRvY3g/driveItem/content?format=pdf

[Original post]

In order to get to the actual file two API calls are needed, one to fetch the drive (library) id, and one to fetch the file.

Note: This solution will not work on the root site collection as I make assumptions on the number of parts of a URL. The following file formats are supported: csv, doc, docx, odp, ods, odt, pot, potm, potx, pps, ppsx, ppsxm, ppt, pptm, pptx, rtf, xls, xlsx.

Deconstructing the file URL

Splitting the URL on slashes we get the parts needed to get the id of the document library and the id of the file.

0 https:
1
2 contoso.sharepoint.com
3 sites
4 pub
5 FooLib
6 lala
7 Document.docx

Part 2 is the tenant hostname, part 3+4 is the site path, part 5 is the document library, and part 6 and out is the item path relative to the document library.

Getting the drive id (id of document library)

Using the sample URL above we combine the sites and drives API’s in one query:

/v1.0/sites/{hostname}:{server-relative-path}:/drives

resulting in the following query where we select id and url

https://graph.microsoft.com/v1.0/sites/contos.sharepoint.com:/sites/asite:/drives?$select=id,weburl

The output of this call are all the libraries in the site.

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives(id,webUrl)",
    "value": [
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_psgYyKuXH2VR7fGsvWPyBOt",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/Documents"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_pv8T5clDnpiRZq2uVmXgGRU",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/FooLib"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_psUQF8PSnx9T7aXwvRalLc_",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/PublishingImages"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_pv01hj6qcWyR5wulob7Lk7-",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/Pages"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_pvEaXdch-3DToEk0qR4g-xx",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/SiteCollectionDocuments"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_ptwBh2OaBQOTbJMXT5jLKwi",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/SiteCollectionImages"
        },
        {
            "id": "b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_pv-q5N0D8gWSLB-0MY7_RS3",
            "webUrl": "https://contoso.sharepoint.com/sites/asite/Translation%20Packages"
        }
    ]
}

Ideally you would use a $filter query to pick out just the library you want, but this is not supported for the drives endpoint, so you need to post-filter yourself.

By filtering out the item which has a webUrl  matching part 2,3 and 4 combined you have the library you are looking for.

Getting the PDF URL for the file

With the id of the document library in hand, it’s time for the next query which will return the URL of the PDF version in a 302 Location header.

/v1.0/drives/{drive-id}/root:/{item-path}:/content?format=pdf

Using the drive id from the previous call together with the document path I end up with the following URL

https://graph.microsoft.com/v1.0/drives/b!H11aFSof8062NsPf4rr-qE3OKQpUIjVEp7PzqdeT_pv8T5clDnpiRZq2uVmXgGRU/root:/FooLib/lala/Document.docx:/content?format=pdf

If you look at the Location header in the returned response you will find something similar to:

https://northeurope1-mediap.svc.ms/transform/pdf?provider=spo&inputFormat=docx&cs=N2FiNzg2….

This is a pre-authenticated URL which can be called directly from anywhere without the need to logging in, and the URL is valid for a few minutes only.

Friday, June 29, 2018

Using PnP PowerShell to inspect the crawl log in SharePoint Online

One of the lesser know features for SharePoint Online is the ability to look at the crawl log. For on-premises SharePoint, scanning the crawl log is something any search troubleshooter knows how to do, and something I’ve often heard is an ask for SharePoint Online.

A typical scenario is someone asking why their file does not show up in search, or why a user profile hasn’t been updated in search. The crawl log won’t solve these issues, but might give you hints and insights for further troubleshooting as you can see any warnings or errors, and see at what time the item in question was last crawled.

Over two years ago I created a SharePoint App which allows you to search the crawl log and trigger re-indexing (https://store.office.com/en-us/sharepoint-online-search-toolbox-by-puzzlepart-WA104380514.aspx), but I figured the time was right to share this with the world – hence the June release of PnP PowerShell introduced the new cmdlet Get-PnPSearchCrawlLog. But if you fancy a UI, the app is still there :)

selected image

Test it for yourself or take a look at the demo I did yesterday at the PnP sig call.

https://youtu.be/BaUfhFYC2tQ?t=40m35s

Tuesday, June 12, 2018

Dynamically load a web part from another web part

I coded a proof of concept last week on how to load a SPFx web part from another web part which I plan to use as pattern for display templates for a search web part I’m planning. By using SPFx parts as render templates we get better control of the code, and avoid script injection on the page.

I figured this approach might be useful for other scenarios so here’s the code to play with :)

The POC loads up two instances of the Modern Script Editor web part, and sets data in it. Which means the modern script editor web part has to be installed on the site you are testing on. Or replace with any of the oob web parts and set the correct properties.

Issues I haven’t gotten to yet as they are not a concern for my scenario are:

  • How to access web part properties of the dynamic loaded web parts in edit mode – can be worked around if the web part has a custom edit UI
  • How to persist the web part data and store it in the main web part – take a look at the serialize method of the ClientSideWebPartManager.
  • The text web part is a special kind of web part, so not sure how to dynamically instantiate it

Full sample can be found at https://github.com/wobba/spfx4fun/tree/master/DynamicLoad

import * as React from 'react';
import styles from './HelloWorld.module.scss';
import { IHelloWorldProps } from './IHelloWorldProps';
import { Guid } from '@microsoft/sp-core-library'
import { ClientSideWebPartManager, IWebPartManagerContext, IWebPartData } from '@microsoft/sp-webpart-base';
import { DisplayMode } from '@microsoft/sp-core-library';

import { IClientSideWebPartManifest } from '@microsoft/sp-module-interfaces';

let _webPartManager: ClientSideWebPartManager;
let _sampleIdOne = "mAdcOW" + Guid.newGuid().toString();
let _sampleIdTwo = "mAdcOW" + Guid.newGuid().toString();
export default class HelloWorld extends React.Component<IHelloWorldProps, {}> {

    public async componentDidMount(): Promise<void> {
        _webPartManager = new ClientSideWebPartManager(this.props.context.host);
        await _webPartManager.fetchWebPartManifests(); // Ensure all manifests are available
        this.addData();
    }

    private async addData() {
        // local webpart properties - in this case props for the modern script editor webpart
        let props = {
            script: "<div>Foo</div>",
            title: "The Modern Script Editor web part!",
            removePadding: false,
            spPageContextInfo: false
        }
        await this.loadWebPart("ScriptEditorWebPart", document.getElementById(_sampleIdOne), props);
        await this.loadWebPart("ScriptEditorWebPart", document.getElementById(_sampleIdTwo), props);
    }

    private async loadWebPart(alias: string, domElement: HTMLElement, webPartProperties: any) {
        let manifests = _webPartManager.getWebPartManifests();
        for (let i = 0; i < manifests.length; i++) {
            const manifest = manifests[i];
            if (manifest.alias === alias) {
                let instanceId = Guid.newGuid().toString();
                let wpManifest: IClientSideWebPartManifest<any> = manifest as IClientSideWebPartManifest<any>;
                let wpData: IWebPartData = {
                    id: wpManifest.id,
                    instanceId: instanceId,
                    title: "",
                    dataVersion: "1.0",
                    properties: webPartProperties
                };

                // Specify any as webpartLoadExtraLogInfo is not defined on the interface and has to be present
                let initialize: IWebPartManagerContext & any = {
                    domElement: domElement,
                    instanceId: instanceId,
                    manifest: wpManifest,
                    displayMode: DisplayMode.Read,
                    webPartData: wpData,
                    webpartLoadExtraLogInfo: {
                    }
                };
                await _webPartManager.loadWebPart(initialize);
            }
        }
    }

    public render(): React.ReactElement<IHelloWorldProps> {
        
        return (
            <div className={styles.helloWorld} >
                <div className={styles.container}>
                    <div className={styles.row}>
                        <div className={styles.column}>
                            <span className={styles.title}>Dynamic loading!</span>
                            <span id={_sampleIdOne}></span>
                            <span id={_sampleIdTwo}></span>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

Monday, May 28, 2018

How to reset a modern home page using PnP PowerShell


Photo by Nikita Kostrykin at Unsplash

Here’s a short snippet which will reset any modifications done to a modern home page back to the default layout.

Basically you clear out the CanvasContent1 field which stores the page layout and contents.

Connect-PnPOnline https://<tenant>.sharepoint.com/sites/mysite
# Get welcome page url
$web = Get-PnPWeb -Includes WelcomePage
# Load the page
$file = Get-PnPFile -Url $web.WelcomePage
# Get the page's item
$item = $file.ListItemAllFields
# Load the item id
Get-PnPProperty -ClientObject $item -Property Id
# Clear the content to reset
Set-PnPListItem -List SitePages -Identity $item.Id -Values @{"CanvasContent1"=$null} -SystemUpdate

Quickly clear Followed sites using PnP PowerShell

In my dev tenant I do a lot of testing with Groups and sites, and this has the effect that my demo user is following a lot of, often with the same name as seen in the image below.

image

The followed sites are actually stored in a list named Social in your OneDrive. This list was introduced with SharePoint 2013, and still alive and kicking. You can access followed sites from the following URL:

https://<tenant>-my.sharepoint.com/personal/<user site>/Social/Sites.aspx

This list also tracks followed documents and people, but that’s not the focus of this post.

First connect to your OneDrive using PnP PowerShell, then execute the code below to remove all followed sites, and be sure to set the correct URL in the first line.

$followedSitesUrl = "/personal/<user site>/Social/Private/FollowedSites"
$sites = Get-PnPListItem -List Social -Query "<View Scope='RecursiveAll'><Query><Where><Eq><FieldRef Name='FileDirRef'/><Value 
Type='Text'>$followedSitesUrl</Value></Eq></Where></Query></View>"
$sites |% { Remove-PnPListItem -List Social -Identity $_.ID -Force }

Once complete, the list is now empty.

image

Tuesday, May 22, 2018

Two SPFx manifest settings you might not be aware of

image

If you take a look at the manifest schema for a SPFx web part there are a couple of settings you might want to take a look at.

hiddenFromToolbox

If hiddenFromToolbox is set to "true", the web part will not be visible in the modern SharePoint toolbox. Very useful for web parts you provision automatically on pages, but don’t want users to add themselves.

supportsFullBleed

If supportsFullBleed is set to "true", the web part can be added to a full page width zone on a modern page in a communication site, spanning from the left margin to the right margin without any white space.

Friday, May 11, 2018

Caution when using $expand with Microsoft Graph

Using the $expand parameter with calls to the Microsoft Graph is very handy. In one API call you can retrieve both the object itself and additional properties.

Two examples are fetching a user and the direct reports, or a group and it’s members.

https://graph.microsoft.com/beta/users/061353c3-af75-4767-9b19-a5bceed85f53?$expand=directReports

https://graph.microsoft.com/v1.0/groups/79958190-024b-4c62-ab55-65dc9a066cac?$expand=members

image

The caution is that when you expand a property which has a collection of values, you will only get the first 20 items returned. This means that if you work in an organization with more than 20 people in it, you should not use $expand if you need all the values, but resort to two calls instead, one for the item, and one for the property you want to expand.

Summary

While using expand is very handy, it’s almost always better to break it into two API calls to avoid having issues if you can expect more than 20 items.

Tuesday, May 8, 2018

How to get the list item from a file URL using PnP PowerShell

image
Photo by Karly Santiago at Unsplash

This is one of the snippets I always forget, but which is very handy in cases where you have the absolute URL to an item in SharePoint. The magic is really line 6 which uses the static method WebUrlFromFolderUrlDirect to get the web URL of the file, so that you can connect properly.

$uri = [Uri]'https://contoso.sharepoint.com/sites/foo/bar/Shared%20Documents/mikael-is-cool.docx'
# Connect to the root site, or any other site
Connect-PnPOnline -Url ($uri.Scheme+'://'+$uri.Host)
$ctx = Get-PnPContext
# Get the web url for the file
$webUrl = [Microsoft.SharePoint.Client.Web]::WebUrlFromFolderUrlDirect($ctx, $uri)
Connect-PnPOnline -Url $webUrl
$fileItem =  Get-PnPFile -Url $uri.AbsolutePath -AsListItem