Wednesday 4 November 2015

_api/me/getrecentdocs stopped working! How can I replace it?

Hi there,

So, as it seems, one of our favourite endpoints stopped working. This morning I had a call from one of our clients saying they were unable to view their recent documents in the homepage.

I did a quick investigation and I faced this strange error when trying to call my-site.sharepoint.com/_api/me/getrecentdocs:












So I started to read about this and it seems that this API stopped working a few days ago (guess nobody noticed before!?). Waiting for a (possible) fix was not an option, so I started to develop a workaround for this.

Here's the code of our previous solution:

        var uri = String.format("{0}/_api/me/getrecentdocs/?$top={1}&$filter=startswith(LinkLocation, '{2}')&$select=FileName,LinkLocation,Application", serverUrl, rowLimit, encodeURIComponent(belowWebUrl));

        // Query
        jQuery.ajax({
            url: uri,
            dataType: "json",
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=nometadata" }
        })

When the request was completed (.done), we were getting the array of object like this:

        // Render results
        var arrayOfDocuments = result.value;

For each object within the array (d), it was simple to get property values, using "d.PropertyName". Ex: d.FileName

This solution was really simple, but since it isn't working any more, let's jump into the workaround!

Solution (using the search api):

Let's build our request url, passing the base Uri, query text, query template, selected properties, row limit and sort it descending, using the last modified date (getting the most recent on top, to achieve the same result).

        var baseUri = serverUrl + "/_api/search/query";
        var queryText = "querytext='*'";
        var queryTemplate = "querytemplate='(AuthorOwsUser:{User.AccountName} OR EditorOwsUser:{User.AccountName}) AND ContentType:Document AND IsDocument:1 AND -Title:OneNote_DeletedPages AND -Title:OneNote_RecycleBin NOT(FileExtension:mht OR FileExtension:aspx OR FileExtension:html OR FileExtension:htm)'";
        var selectProps = "selectproperties='Path,Filename,SPWebUrl'";
        var uri = baseUri + "?" + queryText + "&" + queryTemplate + "&rowlimit=" + rowLimit.toString() + "&bypassresulttypes=false&" + selectProps + "&sortlist='LastModifiedTime:descending'&enablesorting=true"; 
        
        // Query
        jQuery.ajax({
            url: uri,
            dataType: "json",
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=nometadata" }
        })

Now, the way we are going to get our array will be a bit different:

        var arrayOfDocuments = result.PrimaryQueryResult.RelevantResults.Table.Rows; 

In order to have the same easiness on accessing the object's properties (d.FileName), we will have to simplify the data structure, like this:

        // Simplify the data strucutre
        var arrayOfDocumentsSimplified = [];
        _.each(arrayOfDocuments, function (d) {
            var doc = {};
            _.each(d.Cells, function (c) {
                doc[c.Key] = c.Value;
            });
            arrayOfDocumentsSimplified.push(doc);
        });

And that's it! Our most recent documents are showing up again :)





















Hope you find it useful!

RS

Tuesday 18 August 2015

Query office graph in Office Add-in using SharePoint search

I've been playing around with Office add-ins lately, so I decided to write a post about it.

In this example we are going to login to SharePoint using Office 365 APIs, as described by Richard diZerega in this blog post.

To authenticate, we will perform a manual OAuth flow, which was really well described by Chaks here.

Transcribing Richard's description on the authentication process:

"Here are the high-level steps for this flow and cross-window communication:
  1. Check for a user cookie (which maps to a refresh token in a database)
  2. If the user doesn't have a cookie…generate a new GUID and store as cookie
  3. Launch the OAuth flow with Azure AD in a new window (passing the GUID as reference)
  4. Use the authorization code returned from the OAuth flow to get access and refresh token
  5. Store the refresh token in the database with the GUID user reference
  6. Prompt the user to refresh the Office app (which can now lookup the refresh token by the GUID user reference that is stored in a cookie)
  7. Use the refresh token in the app to get resource-specific access tokens for data retrieval"
Here is the script that is launched for unknown users:


And here is the OAuthController for managing the authorization code response from Azure AD: 


To get the access token using the code retrieved by the authentication flow, the following code will be executed:


All the values must be defined in the web.config, as we're retrieving them using our 'SettingsHelper' class:



After the last action, a view will be retrieved, informing the user to close the prompted dialog. Nothing new till now, all of this was already described by Richard.

Since we're authenticated now, let's start building our File's class to store the values we need:


Now we can create our FileRepository class, to implement the method to get the files from SharePoint using search API...


Our 'GetFiles' method will receive a token and a parameter which will indicate what do I want to retrieve. In order to summarize, I will explain the "My Work" query:

query = "/search/query?Querytext='*'&Properties='GraphQuery:AND(ACTOR(391380\\,action\\:1003)\\,ACTOR(391380\\,OR(action\\:1003\\,action\\:1036\\,action\\:1037\\,action\\:1039)))'&selectproperties='Title,Path,ViewsLifeTime,LastModifiedTime,SiteID,WebId,UniqueId,SecondaryFileExtension,SiteTitle,SPWebUrl,ServerRedirectedURL,EditorOWSUSER'";


  • /search/query?Querytext='*' - We're using the search api and defining the text as '*', which is any text.
  • &Properties='GraphQuery:AND(ACTOR(391380\\,action\\:1003)\\,ACTOR(391380\\,OR(action\\:1003\\,action\\:1036\\,action\\:1037\\,action\\:1039))) - Here we're saying that we want to specify properties of a graph query (using actor(s) and action(s)). In this example I'm using my own Actor ID "391380", since we're getting access using app permissions, it won't work if I define the actor as "ME".
  • &selectproperties='Title,Path,ViewsLifeTime(...) - finally, let's bring the properties we need (specified in the File class).
After our request being processed, we'll store the response relevant information:



With all the business logic done, let's talk about controllers and views...

Controller:

Our controller will get the stored token and get the files using the token and a parameter to indicate that I want "My Work" files (Created and/or modified by me)


View:

Let's call the Index action in our view with a fancy button...


Aaand it's done! A little bit of creativity and you can, for example, to have Delve in an Office add-in:



Happy coding!


Thursday 16 July 2015

Downloading files from OneDrive using the Office 365 API (MVC add-in)

For this example I used the "Office 365 Starter Project for ASP.NET MVC" developed by Microsoft, which you can find here, including all the instructions to set it up.

This sample uses "the Office 365 API Tools to demonstrate basic operations against the Calendar, Contacts, and Mail service endpoints in Office 365 from a single-tenant ASP.NET MVC application.", so when you click in "My Files" button, you get a list of all files and folders within your personal OneDrive and the capability of deleting them. 

What I'm going to demonstrate is how to extend those capabilities, by adding a new link (Download) which will allow you to download the files you want. 

First, we have to create the method to download the file in our FileOperations class, located inside the "Helpers" folder.























Then, we have to create our "Download" action in the file controller. This action will be a FileStreamResult, as we want to send binary content using a Stream instance. 
Finally we will return the result of type File, passing the Stream instance and the file name. 

Note: In order to dynamically retrieve the item content type, we will use MimeMapping.GetMimeMapping method.



















To finish, we need to edit the view and add an action link to our controller. This action will be displayed only when the item is a file.














... The result:



























Happy coding ;)

Monday 13 July 2015

JSLink with callback handler and synchronous ajax calls to validate user input.

Scenario:

For this specific case, I based myself on one of our client's requests, which was "to create some sort of validation, when a user submits an item in a booking list". 

There was a list indicating the stock of each item available for booking, so the approach was to request the number of items available (via ajax call) in the mentioned list and if that number was greater than "0", then the user should be able to make a booking.

Details:

Material field name : Material
Item availability list name : Item Availability
Booking list name: Material Bookings

Solution:

After the JSLink file being referenced in the list definition (Schema.xml), we can start developing our business logic.


  • Adding the callback handler to the new form.













  • In the "AddCallbackHandler" function we will have to:

    1. Get form context for current field
    2. Create a validator set to register the required validator and our custom validator
    3. Register our error callback
    4. Return the field default html (choice in this case) and a <span> tag which will contain our custom error message to be displayed when saving a new item is not allowed.
























  • Now let's define our custom validation
I developed a separate function to get the results, just because we needed to make more than one call. Note: "async" was set to false, otherwise it would have to be promisified.


  • To finish, we just have to define what happens when an error occurs.







Find the full code below:



Hope you find it useful ;)