SPFx Extension – Email File URL

In SharePoint there used to be a ‘copy shortcut’ option in the right-click menu on a document. This feature is not available currently. Now we need to go to the share sub-menu to get a link to the document, but what is offered there is the docidredir link, not the full path.

This SPFx extension opens a dialog where the user can see the document full path. The Email Link button helps the user to share the link via default email client.

react-command-email-urlreact-command-email-urlreact-command-email-url

Used SharePoint Framework Version

SPFx Extensions Dev Preview

Applies to

Solution Author(s)
react-command-email-url Joseph Velliah (SPRIDER, @sprider)

Version history

Version Date Comments
1.0 June 12, 2017 Initial release

Disclaimer

THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

Minimal Path to Awesome

  • Clone this repository
  • Move to folder where this readme exists
  • In the command window run:
    • npm install
    • gulp serve --nobrowser
  • Use following query parameter in the SharePoint site to get extension loaded without installing it to app catalog

Debug URL for testing

Here’s a debug URL for testing around this sample.

?loadSpfx=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"0af576ed-1e72-4602-bea9-543c60f5fa09":{"location":"ClientSideExtension.ListViewCommandSet.CommandBar"}}

Full URL to request would be something like following:

https://tenant.sharepoint.com/Shared%20Documents/Forms/AllItems.aspx?loadSpfx=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"0af576ed-1e72-4602-bea9-543c60f5fa09":{"location":"ClientSideExtension.ListViewCommandSet.CommandBar"}}

Features

This project contains SharePoint Framework extensions that illustrates next features:

  • Command extension
  • Office UI Fabric React

Notice. This sample is designed to be used in debug mode and does not contain automatic packaging setup for the “production” deployment.

Building the code

git clone the repo
npm i
npm i -g gulp
gulp

This package produces the following:

  • lib/* – intermediate-stage commonjs build artifacts
  • dist/* – the bundled script, along with other resources
  • deploy/* – all resources which should be uploaded to a CDN.

Microsoft Blocked HTML Markup in SharePoint Calculated Fields

Problem
Microsoft has started blocking execution of custom markup in calculated fields in SharePoint Online from June 13, 2017 onwards.

Reference

Workaround

  • JS Link Approach – JSLink allows us to change the field formatting based on field values or custom logic. This approach may not work for calculated column and modern concept.
  • DOM Manipulation at runtime using a script editor webpart – This is a temporary solution as Microsoft is recommending users to use modern features.
  • SharePoint Framework Extension – This approach can be used only when we switch to modern concepts. We can make a ApplicationCustomizers using extension approach to derive a generic solution.

Temporary Fix for Classic Pages – DOM Manipulation at runtime

Sample Formula

Snip20170617_7

Sample JavaScript Code to add in Script Editor WebPart

Snip20170617_6.png

SharePoint Framework WebPart JQuery – AngularJS Conflict

Issue Summary

I have created a SharePoint Framework WebPart using AngularJS 1.5.8 (not using TypeScript) which was working fine in workbench.html. However I have noticed that we get intermittent errors and WebPart failed to render when a user does things like refresh. We could reproduce this error in all the major browsers.

Errors

TypeError: s.bootstrap is not a function

TypeError: qa.fn is undefined

Analysis

One thing is very clear from the error is that the angular related objects/modules are not recognized when the SPFx WebPart is trying to bootstrap. So I started analyzing the other components involved in the page and I could find the following

  1. Custom JavaScript Injections injected in the page such as global navigation using JQuery 1.11.1
  2. More than one SPFx WebPart using angualr 1.5.8

Options Tried 

  1. Initially I thought it might be an angular load timing/sequence issue and tried to reference angular in the page layout directly but no luck
  2. MS Engineer suggested to load JQuery latest version(2.X or 3.X) for custom actions but still I could not resolve the issue.
  3. I also tried to load the custom actions with JQuery noConflict() method

Root Cause 

As we all aware angular uses the lite version of JQuery internally. If JQuery is not available in the page, angular.element delegates to AngularJS’s built-in subset of JQuery, called jqLite. I noticed the SPFx WebPart was loading in all the browsers consistently when custom actions with no JQuery or without custom actions on the page. This means the JQuery(any version) included in the custom action conflicts with the jqLite included in angular. This was the root cause.

Solution

Now I have 2 options

  1. Remove JQuery from custom actions
  2. Avoid JQuery Conflict in SPFX WebPart

In my case I adopted option 1 because I can replace the JQuery related code with pure Javascript in custom actions and there is no clear documentation about option 2.

As per the MS Engineer, The product group is looking at adding some documentation/samples to show how to do option 2. Let us wait for this update as we can not completely avoid JQuery in SharePoint Development.

Reference

https://github.com/SharePoint/sp-dev-fx-webparts/issues/188

 

 

How to add external JavaScript file without typings in SharePoint Framework

Problem Statement 

In one of my requirement I have to reference https://c64.assets-yammer.com/assets/platform_embed.js file in SharePoint Framework WebPart to display the yammer feeds. But the platform_embed.js file is not available in npm and typings are also not available.

Solution

As the TypeScript typings is not available for yammer platform_embed.js file, we can’t use import * as yammer from the external settings. Instead, we should use require(‘yammer’).

Also yam is a global object, so we have to change the way we reference it to (window as any).yam.connect.embedFeed() instead of (yam as any).connect.embedFeed()

Thank you Waldek Mastykarz for this suggestion.

Check the below example

spfx1

spfx2.PNG

SharePoint Framework – Yammer Feed WebPart using Angular

Summary

This Web Part displays the feeds from yammer using https://c64.assets-yammer.com/assets/platform_embed.js.

Yammer Feed WebPart using Angular Yammer Feed WebPart Properties

Used SharePoint Framework Version

drop

Applies to

Solution

Solution Author(s)
angular-yammer Joseph Velliah (SPRIDER, @sprider)

Version history

Version Date Comments
1.0 March 03, 2017 Initial release

Disclaimer

THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.


Minimal Path to Awesome

  • clone this repo
  • in the command line run:
    • npm i
    • tsd install
    • gulp serve

Features

The Yammer Feed is a sample Client-Side Web Part built on the SharePoint Framework using Angular.

This Web Part illustrates the following concepts on top of the SharePoint Framework:

  • using Angular for building SharePoint Framework Client-Side Web Parts
  • including Angular in the Web Part bundle
  • using non-reactive Web Part Property Pane and custom Properties
  • using conditional rendering for one-time Web Part setup
  • passing Web Part configuration to Angular and reacting to configuration changes
  • display the yammer feed based on configuration changes

Download SharePoint Online Files using MAC CURL Command

Problem Statement
MAC SharePoint users were downloading the files from SharePoint 2007 Document Libraries using the CURL command but the same is not working after migrating the SharePoint 2007 sites to SharePoint Online(SPOL).

Example
curl -k https://domain.sharepoint.com/sites/test/librarytitle/foldertitle/filename.zip --NTLM --user "domain\user":"password" -o filename.zip

Cause
SharePoint Online uses claims based authentication and direct NTLM based curl commands are not allowed.

Resolution
SPOL allows remote applications to call the REST API with user impersonation. This article demonstrates how to access SPOL REST API and download the files from a tenant using Apple Bash Script and Curl commands. However, outside of .NET the authentication piece is not so straightforward. App authentication solves this issue for registered apps but in this document you will see how remote user authentication can be achieved, regardless of platform.

The below diagram illustrates the HTTP requests which need to be made in order to authenticate SharePoint Online.

spolremoteauth

Applies To
Office 365 Tenant connected with Active Directory Federated Service(ADFS) and MAC OS

Execution Steps

  • Download the BashSPOLFileDownload.sh file from here
  • Open BashSPOLFileDownload.sh file in a text editor(TextWrangler/TextMate) and update the UserName (Line #4) and Password (Line #5).

remotecodebash

Note: The UserName & Password provided should have access to download the file from SharePoint Online.

  • To download the file from a different SharePoint Online site/library/folder/file where the account has access, change the values of EndPoint(Line #6) and FileServerRelativeUrl(Line #7) values.
  • Save the BashSPOLFileDownload.sh file
  • Open Terminal (command line tool) and go to the path where BashSPOLFileDownload.sh file is saved
  • Execute the following command to convert the BashSPOLFileDownload.sh file executable chmod 700 BashSPOLFileDownload.sh
  • Run the script using just the name of the script(Example : ./ BashSPOLFileDownload.sh)
  • If all goes well, you should be able to see the downloaded file in the output path given in OutputFilePath(Line #9).

Failure Chances

  • UserName and Password provided might be wrong
  • Access denied from SharePoint Online for the UserName and Password provided
  • Provided Site/Document Library/Folder/File is not available in SharePoint Online or wrong
  • Text Editor might have changed or corrupted the BashSPOLFileDownload.sh file while saving

ASP.NET Custom Web API & Curl Bash and PowerShell commands to download files from SharePoint Online

In this article I am going to illustrate how to download a file from SharePoint Online using Custom ASP.NET Web API and Curl Bash Command.

Model Definition

using System.ComponentModel.DataAnnotations;
using SPOL.FileDownload.Filters;

namespace SPOL.FileDownload.Models
{
public class SPOLFile
{
[Required(ErrorMessage = “Site Url is not passed with the endpoint”)]
[Display(Name = “SiteUrl”)]
[ValidateSite]
public string SiteUrl { get; set; }

[Required(ErrorMessage = “Library Name is not passed with the endpoint”)]
[Display(Name = “LibraryName”)]
[ValidateLibrary]
public string LibraryName { get; set; }

[Display(Name = “FolderPath”)]
[ValidateFolder]
public string FolderPath { get; set; }

[Required(ErrorMessage = “File Name is not passed with the endpoint”)]
[Display(Name = “FileName”)]
[ValidateFile]
public string FileName { get; set; }
}
}

Model Validation

using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace SPOL.FileDownload.Filters
{
public class ValidateSPOSiteModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}

Model Field Validation

using System.ComponentModel.DataAnnotations;
using SPOL.FileDownload.Helpers;
using Microsoft.SharePoint.Client;

namespace SPOL.FileDownload.Filters
{
public class ValidateSiteAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
try
{
ClientContext clientContext = null;
var url = value as string;

if (string.IsNullOrEmpty(url))
return false;

clientContext = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(url, Settings.serviceAccountUserId, Settings.serviceAccountPassword);
var web = clientContext.Web;
clientContext.Load(web);

try
{
clientContext.ExecuteQuery();
return true;
}
catch
{
ErrorMessage = “Site url you have passed in not valid.”;
return false;
}
finally
{
if (clientContext != null)
clientContext.Dispose();
}
}
catch
{
ErrorMessage = “There is a problem accessing the site url.”;
return false;
}
}

}
}

Model Field Validation with Validation Context to pass parameters

using System.ComponentModel.DataAnnotations;
using SPOL.FileDownload.Helpers;
using Microsoft.SharePoint.Client;
using System;
using System.Reflection;
using System.IO;

namespace SPOL.FileDownload.Filters
{
public class ValidateFileAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
bool isValid = true;
string ErrorMessage = "";
ClientContext clientContext = null;

try
{

var filename = value as string;

object instance = validationContext.ObjectInstance;
Type type = instance.GetType();

PropertyInfo propertySiteUrl = type.GetProperty("SiteUrl");
PropertyInfo propertyLibraryName = type.GetProperty("LibraryName");
PropertyInfo propertyFolderPath = type.GetProperty("FolderPath");

object siteUrl = propertySiteUrl.GetValue(instance);
object libraryName = propertyLibraryName.GetValue(instance);
object objFolderPath = propertyFolderPath.GetValue(instance);

string folderpath = Convert.ToString(string.IsNullOrEmpty((string)objFolderPath) ? "" : objFolderPath);

if (string.IsNullOrEmpty(filename))
{
isValid = false;
ErrorMessage = "File name is not passed with the endpoint";
}

string extention = Path.GetExtension(filename);

if (string.IsNullOrEmpty(extention))
{
ErrorMessage = "Not a valid file name extention.";
isValid = false;
}
else
{

clientContext = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(siteUrl.ToString(), Settings.serviceAccountUserId, Settings.serviceAccountPassword);

var url = new Uri(siteUrl.ToString());
var relativeUrl = String.Format("{0}/{1}/{2}/{3}", url.AbsolutePath, libraryName.ToString(), folderpath, filename);

try
{
var fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(clientContext, relativeUrl);
isValid = true;
}
catch
{
ErrorMessage = "File name you have passed in not avilable.";
isValid = false;
}
finally
{
if (clientContext != null)
clientContext.Dispose();
}
}
}
catch
{
ErrorMessage = "There is a problem accessing the file.";
isValid = false;
}

if (isValid)
return ValidationResult.Success;
else
return new ValidationResult(ErrorMessage);
}
}
}

Controller

using SPOL.FileDownload.Filters;
using SPOL.FileDownload.Helpers;
using SPOL.FileDownload.Models;
using Microsoft.SharePoint.Client;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web;
using System.Web.Http;

namespace SPOL.FileDownload.Controllers
{
public class DownloadFileController : ApiController
{
[Route(“api/file”)]
[HttpGet]
[ValidateSPOSiteModel]
public HttpResponseMessage DownloadFile([FromUri] SPOLFile spolFile)
{
MemoryStream stream = new MemoryStream();
ClientContext clientContext = null;

try
{
if (ModelState.IsValid)
{
clientContext = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(spolFile.SiteUrl, Settings.serviceAccountUserId, Settings.serviceAccountPassword);

var url = new Uri(spolFile.SiteUrl);
var relativeUrl = String.Format(“{0}/{1}/{2}/{3}”, url.AbsolutePath, spolFile.LibraryName, spolFile.FolderPath, spolFile.FileName);

var fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(clientContext, relativeUrl);

var buf = new byte[1024 * 16];
int byteSize;

while ((byteSize = fileInfo.Stream.Read(buf, 0, buf.Length)) > 0)
{
stream.Write(buf, 0, byteSize);
}

string mediaType = MimeMapping.GetMimeMapping(spolFile.FileName);

var result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new ByteArrayContent(stream.ToArray());
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(“attachment”);
result.Content.Headers.ContentDisposition.FileName = spolFile.FileName;
result.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType);

return result;
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
}
catch (Exception ex)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.ToString()); // new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
finally
{
if (stream != null)
stream.Dispose();
if (clientContext != null)
clientContext.Dispose();
}
}

}
}

 

Bash Command

endpoint='http://site.azurewebsites.net/api/file'
siteurl='https://site.sharepoint.com/sites/dev'
libraryname='Files to Download'
folderpath='First Level/Second Level'
filename='Azure_Application_Insights.pptx'

url=”${endpoint}?siteurl=${siteurl}&libraryname=${libraryname}&folderpath=${folderpath}&filename=${filename}”

curl “${url}” -o “${filename}”
PowerShell Command
The same operation can be done in windows OS using PowerShell commands as well.

Invoke-RestMethod -Uri "http://site.azurewebsites.net/api/file?siteurl=https://site.sharepoint.com/sites/dev&libraryname=Files to Download&folderpath=&filename=Azure_Application_Insights.pptx" -Method Get | Out-File -filepath "C:\Users\joseph\Downloads\Azure_Application_Insights.pptx"