SPFx web parts from different solutions on workbench

workbench customizer

Ever wondered how to add SharePoint Framework web parts from different solutions to your local workbench while developing on localhost? Then look no further 🙂

Even though this may not be a common requirement for everyone, there are cases where it could be handy to have different web parts running on the local workbench that belong to different solutions.
I was sitting on a plane without WiFi and was testing some things with a web part running locally. Was having some issues with the workbench page styles, so I tried to find a way to load the Workbench Customizer web part on the Workbench page, without adding it to my solution. It turned out to be fairly simple.

If you never heard about the Workbench Customizer web part, you can read more on this post.

Bundle the “external” solution

The first thing that you will have to do is go to the solution folder containing the web part that you want to load on the main solution and bundle it. Your solution needs to build for this to work. This will create the dist and lib folders, that will contain the files you need to copy to the other solution.
To bundle, we simply run the SharePoint Framework bundle task “gulp bundle

Copy the “external” solution to your main solution

Next, we need to copy some files from the “external” solution to the folders of your main solution. If your main solution does not have bin and dist folders, you can simply run “gulp bundle” to generate them.
All the files that we are going to copy are located in the dist and lib folders. The folders are locally generated and never included as part of your solution in your Git repository. Additionally, you can simply run the clean task (gulp clean) to remove everything.
In order for the web part to work, you will have to copy two folders:

  • dist – open the dist folder on the “external” solution and copy all the files to the dist folder of your main solution
  • lib>webparts>{your web part name} – open the lib>webparts folder on the “external” solution and copy the folder with the same name as your web part to the lib>webparts folder of your main solution. You should see different folders, displaying the different elements of your solution.

Serve your main solution

Everything is done and you can now run the main solution and add both web parts to the Workbench page. To run the main solution locally, simply run the task “gulp serve” as you would normally do.

You can add both web parts to the page and confirm that both work

Package your main solution

You can safely package your main solution while still having the files for the “external” solution on the dist and lib folders. Those will not be included on the package file 🙂

Cleaning up the main solution

If you want to remove all the files from the “external” solution on the main one, simply run “gulp clean”. It’s that easy.

SPFx Workbench Customizer

workbench customizer web part preview

I recently published a blog post about a web part that I use on the workbench page during development. I have this solution deployed on my dev tenant and simply add it to the bottom of the Workbench page. It allows me to work around some workbench limitations when building the UI of SPFx web parts.

I have recently updated this web part and added it to the PnP web part samples project on GitHub. You can get the code here:

/2019/01/18/spfx-workbench-customizer/

The previous version of the web part allowed you to enable/disable specific CSS overrides. Those overrides were dynamically imported on the page based on your settings. When all the overrides were enabled, the Workbench page would have an interface similar to a modern page. For this to happen, the web part would remove extra margins and add extra styles.

Additionally, the web part now also has a switch button to enable page preview mode by default. When this is enabled, right after the page loads, the web part simulates a click on the Preview button at the top of the page and the editing experience is replaced with the preview.

This is really great when testing smaller screen resolutions as the workbench no longer displays a message telling you to “Widen your browser window”. You can see the difference on the images below

This image has an empty alt attribute; its file name is image-1.png

Give it a try and let me know if you have any feedback. Happy coding 🙂

Getting the teams a user is a member of via MS Graph

The Microsoft Graph endpoints for Teams are not something new. You can easily find online multiple blog posts containing sample requests on how to retrieve the teams that a user is a member of. Instead, in this blog post, I will share some code blocks that I used to accomplish this on a SharePoint Framework project.

A sample SharePoint Framework web part solution (react-my-teams) can be found on the PnP Web Part Samples repository once the pull request is merged.
Feel free to use the solution as a starting point and implement the user interface to your needs. And you are also welcome to submit updates to it 🙂

Setting required API permissions

The SharePoint Framework project will use the MSGraphClient to retrieve data from Microsoft Graph. In order to be allowed to get the data we need, we will need to request the following permissions (declared on package-solution.json):

"webApiPermissionRequests": [
  {
	"resource": "Microsoft Graph",
	"scope": "User.ReadWrite.All"
  },
  {
	"resource": "Microsoft Graph",
	"scope": "Group.ReadWrite.All"
  }
]

Alternatively, you can use the Office 365 CLI to grant the required permissions. This blog post will give you all the info you need.

Get Tenant information

In order to open Teams channel links on the client application, you need to include the tenant Id as one of the URL parameters. You can get the Tenant Id using the code below

private _getTenantInfo = async (): Promise<ITenant> => {
    let tenant: ITenant = null;
    try {
      const tenantResponse = await this._graphClient.api('organization').select('id').version('v1.0').get();
      tenant = tenantResponse.value as ITenant;
      console.log(tenant);
    } catch (error) {
      console.log('Error getting tenant information');
    }
    return tenant;
  }

Get teams a user is a member of

The me/joinedTeams Graph endpoint will return the teams for the user. The following code block will allow you to get the data

private _getTeams = async (): Promise<ITeam[]> => {
    let myTeams: ITeam[] = [];
    try {
      const teamsResponse = await this.props.graphClient.api('me/joinedTeams').version('v1.0').get();
      myTeams = teamsResponse.value as ITeam[];
    } catch (error) {
      console.log('Error getting teams');
    }
    return myTeams;
  }

Get team channels

Next, you will need to retrieve the channels for a given team. This will allow us to get the information of the default channel (General) that we will use on the links.

  private _getTeamChannels = async (teamId): Promise<IChannel[]> => {
    let channels: IChannel[] = [];
    try {
      const channelsResponse = await this.props.graphClient.api(`teams/${teamId}/channels`).version('v1.0').get();
      channels = channelsResponse.value as IChannel[];
    } catch (error) {
      console.log('Error getting channels for team ' + teamId);
    }
    return channels;
  }

Dynamically building the links on click

To improve performance on page load, we only load the list of teams by default. The channels information is only loaded when the user clicks on the link, which is then used to generate the final link that will take the user to Teams.

The link will look something like this:

<a href="#" title='Click to open channel' onClick={this._openChannel.bind(this, team.id, this.props.tenantId)}>
  <span>{team.displayName}</span>
</a>

And the following code will generate the required Teams link and open the selected team on Teams (browser or client App)

  private _openChannel = async (teamId: string, tenantId: string): Promise<void> => {
    let link = '#';

    const teamChannels: IChannel[] = await this._getTeamChannels(teamId);
    const channel = teamChannels[0];

    if (this.props.openInClientApp) {
      link = `https://teams.microsoft.com/l/channel/${channel.id}/${channel.displayName}?groupId=${teamId}&tenantId=${tenantId}`;
    } else {
      link = `https://teams.microsoft.com/_#/conversations/${channel.displayName}?threadId=${channel.id}&ctx=channel`;
    }

    window.open(link, '_blank');
  }

Enjoy!

SPFx Workbench Customizer

custom-workbench

With SharePoint Framework, Microsoft also introduced a really good development story for creating custom web parts: the Workbench page.
This page is not only available when you are developing solutions locally, but also on a SharePoint site. This gives you the option to access data on a SharePoint site from code running on your machine. Let’s be honest, it’s great!

Unfortunately, the Workbench page also has some limitations for some development scenarios. One of them being, in my opinion, how the overall page styles differ from a normal modern SharePoint page.

SharePoint Workbench page

As you can see on the image above, the styles are really not great. I personally don’t find a reason why they differ so much from a modern page, and why, for example, the page layout is limited to a maximum width of 924px .

When building custom web parts, I had previously (in some occasions) included some code into the project to deal with this in different ways. But this was never a great approach, as I used to comment/delete that code after the work was done…

And this led me to think on a better solution that would allow me to not having to worry about doing it again. So I thought on just creating a web part for it and always add it to the Workbench whenever I’m working on it.

Workbench Customizer web part

The image above is an example of the workbench page when the web part is added to it (the text on the page is just multiple text web parts to test different zones).

When added to the workbench page, the web part will apply the following changes by default:

  • Change the max page width, allowing the editable area to be the same width as a modern page (1236px)
  • Center the canvas zones on the page, in line with modern pages
  • Update overflow, allowing the scrollbar to appear on the right side of the page
  • Remove additional padding that is introduced by having the page in edit mode by default

Every of the items above is controlled by a web part property that can simply be disabled on the web part properties panel. This is currently achieved using dynamic imports. Every customisation is kept on a separate SASS file that is dynamically imported when the property is enabled. When a property is disabled, a message will be displayed asking you to refresh the page.
I will be looking on improving the experience over time, but it does it’s job at the moment…

Global CSS overrides? Is this not bad?

The CSS changes to the overall page are done using the :global approach. I absolutely know this is not a recommended approach for customising SharePoint, but remember that we are only customising the Workbench page! Who cares! If it breaks, you can simply take it out of the page…

Code

The web part is obviously open-source and is currently on my personal GitHub account
https://github.com/joelfmrodrigues/spfx-workbench-customizer

I’m planning on submitting a PR to the PnP web part samples repository over the next days and will update the post if the PR is accepted.

Update: The web part is obviously open source and is available under the PnP web part samples repository.

If you have any ideas on things that may be missing or any feedback about the current implementation, please reach out. Any feedback is welcome.

Hope you find it useful and use it while building your own web parts.

I have published another blog post with an update. You can read more here: SPFx Workbench Customizer update

Add Google Analytics to SharePoint modern pages

ga-sp

I had a client requirement to help them add Google Analytics to a modern SharePoint site. The objective was to track all SharePoint page views within the site.

The first thing that came to my mind was to look for a solution available online. As this is a fairly common scenario, I assumed it would be easy to find one for my requirements. But this was also what the client had tried to do before calling us, and they got stuck with some limitations on the solutions that they have found.
There are plenty of solutions available online for using Google Analytics with SharePoint modern pages. From complete implementations, to blog posts with the relevant code snippets. But I was also unable to find one that was able to track full and partial page loads. And so I decided to tweak one to work on the scenarios.

Base solution

The code below is based on the original solution provided by João Ferreira on this blog post. He uses an SPFx application customizer to load the Google Analytics script on every SharePoint page. When testing it, we found that it was not tracking all the page loads, so we updated it slightly.

Additionally, the this.context.application.navigatedEvent event currently has a known bug and fires twice. The workaround provided by jonthenerd worked fine in this scenario.
2019-03-01 update: The bug on the navigatedEvent seems to be fixed now, so I have updated the code below. I have also sent a PR with the changes to the original repository (by João Ferreira). You can find a full implementation project on his post.

Code

The following code uses the approach provided by Google Analytics for tracking Single Page Applications, like SharePoint pages that only load partially..

Hope you fins this useful, and feel free to leave comments or provide feedback.

export default class AnalyticsApplicationCustomizer
  extends BaseApplicationCustomizer<IAnalyticsApplicationCustomizerProperties> {

  private isInitialLoad = true;

  private getFreshCurrentPage(): string {
    return window.location.pathname + window.location.search;
  }


  private navigatedEvent(): void {

    let trackingID: string = this.properties.trackingID;
    if (!trackingID) {
      Log.info(LOG_SOURCE, `${strings.MissingID}`);
    } else {

      if (this.isInitialLoad) {
        this.initialNavigatedEvent(trackingID);
        this.isInitialLoad = false;

      }
      else {
        this.partialNavigatedEvent(trackingID);
      }
    }
  }

  private initialNavigatedEvent(trackingID: string): void {

    console.log("Tracking full page load...");

    var gtagScript = document.createElement("script");
    gtagScript.type = "text/javascript";
    gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${trackingID}`;
    gtagScript.async = true;
    document.head.appendChild(gtagScript);

    eval(`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config',  '${trackingID}');
        `);
  }

  private partialNavigatedEvent(trackingID: string): void {

    console.log("Tracking partial page load...");

    eval(`
        if(ga) {
          ga('create', '${trackingID}', 'auto');
          ga('set', 'page', '${this.getFreshCurrentPage()}');
          ga('send', 'pageview');
        }
        `);
  }

  @override
  public onInit(): Promise<void> {

    this.context.application.navigatedEvent.add(this, this.navigatedEvent);

    return Promise.resolve();
  }
}

SPFx solution using PnPjs for Project Online REST API

SPFx Project SharePoint

If you know me or follow me on Twitter/LinkedIn, you must have realized by now how much I like the PnPjs library. Enough to venture myself to speak about it on 3 SharePoint Saturday events last year. The library has packages for SharePoint and Graph endpoints and can be easily used on SPFx solutions. But if you need an SPFx solution that consumes Project Online API, what options do you have?
Kudos to Paweł Hawrylak who started creating the Project module for PnPjs and currently already offers support for a wide range of endpoints. The module is currently in a dev branch and requires additional work and testing, but it’s already a phenomenal effort.

This blog post will cover the required steps to generate a local PnPjs Project package to consume Project Online REST APIs and create a SPFx web part that uses it.

Published npm packaged

2019-12-31 update
If you only want to give the current version a try, there is now a packaged published to npm that you can simply install. Don’t forget to also install the required dependencies of @pnp/common, @pnp/odata, @pnp/logging and you are ready to rock!

npm i pnpjs-project-online-package @pnp/common@1.3.7 @pnp/logging@1.3.7 @pnp/odata@1.3.7 --save-exact

Get the code from GitHub

2019-11-14 update: I now have a repository forked from the main PnPjs project where I added the code for the project module. The good news is that this is using the latest PnPjs version (1.3.7 at the time)

https://github.com/joelfmrodrigues/pnpjs/tree/feature/project-online-api

From here, you can download, clone or fork it, what ever makes sense for what you plan to do with it. Just be aware that if you plan to fork it to contribute, and you have also previously forked the original PnPjs repository, you may need to delete the last and fork after that.
You can find the original code under Paweł’s GitHub fork of the main PnPjs project on GitHub.

Build PnPjs packages

Since we have the PnPjs source code, we need to build and generate local packages. You can get additional information of the PnPjs gulp commands on the official documentation.

Run the following commands in the order specified:

  1. npm install – to install the npm packages required by PnPjs
  2. gulp build – to ensure that the solution can build successfully
  3. gulp package – to generate all the different library packages, including the new Project package

Peer dependencies

We are going to install the local Project package that we have just build in our SPFx solution. Unfortunately, the peer dependencies don’t seem to work as expected when you do so and you get errors when trying to use the packages on your web part.
If you inspect the package.json file for the Project package created (dist/packages/project), you can find the following required peerDependencies which are not resolved by default

  "peerDependencies": {
        "@pnp/common": "1.3.7",
        "@pnp/logging": "1.3.7",
        "@pnp/odata": "1.3.7"
    },

Not the ideal solution for sure, but a simple way to get around this problem is to simply install the packages yourself.
Remember that we are only testing the new Project package and all the others are kept untouched, so seems sensible to me to install them directly from npm.
Ensure that your command prompt is on the
dist/packages/project directory and run the following command

npm install @pnp/common@1.3.7 @pnp/logging@1.3.7 @pnp/odata@1.3.7

The Project package is now ready to be consumed by our solution.

Create new SPFx solution

I’m not going to provide any specific instructions here as the official documentation is excellent and give you all the information you need. If you are new to SharePoint Framework development, please check it out and learn how to get started.

Add PnPjs packages to SPFx solution

Again, remember that Project is the only package that is not published to npm, so is the only one that we need to install from a local path.
Start by installing all the common PnPjs packages that you usually install. In this case I’m using the 1.2.5 version as it’s the version that matches the local version.

npm install @pnp/common@1.3.7 @pnp/logging@1.3.7 @pnp/odata@1.3.7 --save-exact

Next, install the local Project package by providing a relative path to the package folder. In my example:

npm install ../PnPjs/dist/packages/project

Establish Context

Following the official guidance, we also need to establish the context when using the library in SPFx. Simply add the following block of code into your web part main file as provided on the documentation:

import { project } from "pnpjs-project-online-package"; // or import from your local package for development

// ...

public onInit(): Promise<void> {

  return super.onInit().then(_ => {

    // other init code may be present

    project.setup({
      spfxContext: this.context,
      project: {
        baseUrl: 'https://XXXXXXXXXX.sharepoint.com/sites/pwa'
      }
    });
  });
}

// ...

Please note that we are setting the baseUrl property to be the PWA site. This is to allow the solution to work from any SharePoint sites, not only Project Online.

Use it!

It’s all done and you can now use the fluent library to interact with project online!
All you need to do is to import the Project package when you need it

import { project } from "@pnp/project";

Some usage examples:

// get all projects
const projects = await project.projects.get();

// get projects, filtering by name, returning only the Id and Name properties, and limiting the results count to 1
const projectInfo = await project.projects.filter(`Name eq '${projectName}'`).select('Id,Name').top(1).get();

// create timesheet
const createResult = await project.timeSheetPeriods.getById('XXXXXXXXXXXXXXXX').createTimeSheet();

Style SPFx workbench

This blog post does not contain anything amazing or new. But it’s something I use for a very long time and decided to write it down to hopefully help someone. Style the SPFx workbench page.

Note: I have since created a workbench customizer web part that you can just add to the workbench page. You can read more here. And you can find the web part here.

The SPFx Workbench page is fantastic. You can use the local version to run your code locally, or you can use the online hosted version to test your code against your site. But it has some limitations.
One of the limitations I often find is related to the maximum width. This is mainly a problem with web parts that need to be able to adapt to wider web part zones. The Workbench page is set to have a max-width of 924px by default on one of the top page elements, preventing the middle area of the page from expanding.
But as we are talking about CSS, it can be easily fixed with more CSS.

The :global prefix

When the SPFx build tools generate CSS classes from your SASS files, it changes the name of the classes to make them unique and ensure that you do not override any CSS class already on the page with that name. But what if you want to override CSS? (Please don’t do it on normal pages as it’s not recommended!)
In that case, you can make use of the :global attribute. By prefixing your styles with :global, you can ensure that they will not be renamed by the build tools.

You can take advantage of this attribute to change the styles of the Workbench page. This is only a page targeted for developers, so it’s not the end of the world if the OOB styles are changed and your overrides break. You can just update them and fix the issue if that ever happens.

The following snippet allows you to control the max width of the page, by overriding the default 929px value applied to that element.


:global #workbenchPageContent {
    max-width: 1284px;
}

But the above is only an example. You can have a look around and override other styles if that makes sense for your case. Just make sure you do so in a way that your CSS overrides don’t get applied to other pages when your web part is deployed.

Using pnpm with SPFx 1.7.0

pnpm error

Update 21-11-2018: As documented on the pnpm FAQs, a simpler way to use pnpm is to use the “–shamefully-flatten” flag. This will create a flat node_modules structure similar to npm and yarn.
The instructions below are relevant if you do not want to use the “–shamefully-flatten”, but be aware that you will end up modifying the package.json file.

Original Post

About 2 weeks ago, I published a blog post on how to use pnpm with SPFx 1.6.0 and the required steps to have a working solution.
Today, I was reviewing the comments and noticed someone was having issues with SPFx 1.7.0, so decided to give it a go.

Disclaimer: tested on Windows 10 only, using SPFx 1.7.0.

When you scaffold a new SPFx 1.7.0 project using pnpm, you will get the following error when trying to build it

Inspecting the tslint.json file made it clear that there was some changes between SPFx 1.6.0 and 1.7.0:

SPFx 1.6.0

SPFx 1.7.0

Opening the base-tslint.json file referenced, we can see tslint-microsoft-contrib under rulesDirectory. 

Taking the same approach as per the article for SPFx 1.6.0, I installed the following modules

pnpm i tslint-microsoft-contrib@5.2.1 -DE
pnpm i tslint@5.9.1 -DE
pnpm i typescript@2.4.2 -DE

And voilà, the project is now building without errors

Resources from my session at SPS Leicester 2018

SPS Leicester

Last weekend, I had the pleasure of speaking at SharePoint Saturday Leicester (SPS Leicester). Very well organized event and with a good number of attendees, especially considering that it was the first event.

You can find the slides from my presentation below.

All the code is available under my GitHub account here and there is a readme file with all the steps that I covered during my demo here.

Please get in touch or leave a comment below if you have any issues or questions.


Using pnpm with SPFx 1.6.0

Disclaimer: tested on Windows 10 only, using SPFx 1.6.0.
Looking for SPFx 1.7.0? Have a look at this blog post.

When you create a new SharePoint Framework project, you have the option to use different package managers: npm, pnpm or yarn.

For a long time, I completely ignored this and just used npm. Npm is the slowest option from the list above, but it didn’t really matter as I was installing packages once a week or so. But this is not the case anymore. Simple processes, like upgrading your existing solutions to newer versions of the SharePoint Framework can make you go through that process more times than desired.

In my case, my dev laptop takes a very long time to install or delete node modules, so I found myself looking for alternative solutions.

From the three package managers mentioned above, pnpm was the one that in my opinion had a better chance to resolve my problem, so I decided to give it a go.

pnpm and SPFx 1.6.0

To scaffold a new SPFx project using pnpm, simply run:

yo @microsoft/sharepoint --package-manager pnpm

yeoman

When you finish going through the generator options, pnpm will start installing the modules in the same way as npm (or yarn). But the difference here is down to how pnpm works. You can read more here.

The first time you do this, it will download the modules, so expect some time to be required. But where pnpm really shines is on consecutive runs. I ran the generator a second time and it took less than 2 minutes to complete installing the modules! Using npm was taking much longer than that (around 10 to 15)!

As you can see on the following image, the second run reused all the existing packages previously downloaded:

pnpm-reused-packages

So all was looking great…until I tried to build the project. Unfortunately there are errors as the build pipeline fails to find the tsling and typescript modules. The good thing is that this is a known problem and is well documented here (kudos to Andrew Connell who shared this on Twitter a while ago).

I decided to adopt the solution to add the faulty packages as dependencies to the project (solution 1 from the article linked above) so executed the following commands to install both packages as Dev dependencies. Please note the specific versions that match the ones used by SPFx 1.6.0.

pnpm i tslint@5.9.1 -DE

pnpm i typescript@2.4.2 -DE

And that’s it! Now my solution builds and runs as expected

pnpm-build

But wait, there’s more 🙂 deleting the solution or the node_modules folder also takes less time.

I hope you find this useful, especially if you have a slow machine 🙂