Sunday, 16 November 2014

Summary Web part for all Tasks Assigned to the Current User

This post walks through a roll-up web part that summarises all Active tasks, Assigned To the current user, from across the entire SharePoint Online tenant, or On-Prem farm.

The below solution has been documented for a SharePoint Online tenant, but it could easily be used for an On-Prem install if required. Additionally, this could also be adapted to a SharePoint-Hosted App as an App Part, but for this blog I have just stuck to the simplest form to demonstrate the code.

THE SCENARIO:

  • SharePoint Online/Office365
  • Create a web part to return all non-completed tasks, Assigned To the current user, from all Task lists, from all site collections in the tenant
  • Present these tasks in a rolled up summary table with the COUNT of items per task list displayed
  • And link to the list if the user wants to see their Assigned To items

THE FINISHED PRODUCT:


If this looks like what you're trying to achieve, then read on!

THE SOLUTION:

I implemented the solution using SharePoint's Content Search Web Part (CSWP) and a custom Display Template.

Step 1: Create the custom Display Template

OK, I am not going to go into the inner workings of the Content Search Web Part. It is a fantastic feature in 2013 and there is plenty of great stuff out there to get you started. Like these pages here and here.

Note, just in case I have missed anything, you can download working versions of this code from my GitHub Gist.

So let's go!

The Control File

Generally the Control File doesn't do a lot of heavy lifting. It just renders the headers of the table and leaves the rest of the logic to the item file.
But in this case, we are going to iterate through the entire result set returned by search, group these results into site and list groupings, then send this custom result set to the Item file for processing.
  • First, take a copy of one of the existing Control Templates: Control_List.html (https://site/_catalogs/masterpage/Display Templates/Content Web Parts/Control_List.html)
  • Set the Managed Property Mapping:
  • <mso:ManagedPropertyMapping msdt:dt="string">&#39;Link URL&#39;{Link URL}:&#39;Path&#39;,&#39;Task Title&#39;{Task Title}:&#39;Title&#39;,&#39;SecondaryFileExtension&#39;,&#39;ContentTypeId&#39;,&#39;ListItemID&#39;,&#39;SiteTitle&#39;,&#39;SitePath&#39;,&#39;Site&#39;,&#39;SPSiteURL&#39;,&#39;ListUrl&#39;</mso:ManagedPropertyMapping> 
    
  • Then, add the required reference to the CSS file that we'll fill out below:
    <script>
        $includeLanguageScript(this.url, "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
        $includeCSS(this.url, "~sitecollection/Style Library/css/custom.css");
    </script>

  • Include the following in the JavaScipt:
    var encodedID = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_Table_");
  • replace everything after var noResultsClassName = "ms-srch-result-noResults"; with
 _#-->  
           <table class="dataTable" id="_#= encodedID =#_">  
             <thead>  
               <tr>  
                 <th>Site Name</th>  
                 <th>List Name</th>  
                 <th align="center">Assigned to me</th>  
               </tr>  
             </thead>  
             <tbody>  
                       <!--#_  
                var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_row_");  
                var linkId = encodedId + "link";  
                var titleId = encodedId + "title";  
                var siteId = encodedId + "site";   
                var listData = ctx.ListData;  
                var resultcount = listData.ResultTables[0].ResultRows.length;  
                var resultArray = [];  
                resultArray = listData.ResultTables[0].ResultRows;  
                var other = {},  
                     i;  
                for (i=0; i < resultArray.length; i++) {  
                     ln = ctx.ListData.ResultTables[0].ResultRows[i].Path.toString().toLowerCase().search("/dispform.aspx");  
                     listURL = ctx.ListData.ResultTables[0].ResultRows[i].Path.toString().substring(0,ln)  
                  // if other doesn't already have a property for the current letter  
                  // create it and assign it to a new empty array  
                  if (!(listURL in other))  
                   other[listURL] = [];  
                     // send the object to the Other array:  
                  other[listURL].push(resultArray[i]);  
                }  
                for(var key in other){  
                     var taskcount = other[key].length;  
                     var len = other[key][0].Path.toString().toLowerCase().search("/dispform.aspx");  
                     var listURL = other[key][0].Path.toString().substring(0,len)  
                     var siteTitle = other[key][0].SiteTitle;  
                  var len2 = listURL.toString().lastIndexOf("/") + 1;  
                  var listTitle = listURL.toString().substring(len2,listURL.length);  
                     ms_outHtml.push(''  
                                         ,'  '         
                                         ,'          <tr class="border_bottom ms-itmHoverEnabled">'  
                                         ,'         <td>', siteTitle, '</td>'  
                                         ,'         <td><a href="', listURL ,'">', listTitle ,'</a></td>'  
                                         ,'         <td align="center">', taskcount ,'</td>'  
                                         ,'          </tr>'  
                                         ,''  
                                         );  
                     }  
                _#-->  
             </tbody>  
           </table>  
   </div>  
 </body>  
 </html>  

The Item File

  • Again, start with a copy of the following template: Item_TwoLines.html
  • Replace everything after, and including <div id="TwoLines"> with:
  <div id="Item_TaskDataRow">  
           <!--#_  
                var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_row_");  
                var siteTitle = $getItemValue(ctx, "SiteTitle");  
                var siteURL = $getItemValue(ctx, "SPSiteURL");  
                var itemPath = $getItemValue(ctx, "Path");  
                var n1 = itemPath.toString().toLowerCase().search("/dispform.aspx");  
                var listURL = itemPath.toString().substring(0,n1);  
             var n2 = listURL.toString().lastIndexOf("/") + 1;  
             var listTitle = listURL.toString().substring(n2,listURL.length);  
                var linkId = encodedId + "link";  
                var titleId = encodedId + "title";  
                var siteId = encodedId + "site";   
                var listData = ctx.ListData;  
                var resultcount = listData.ResultTables[0].ResultRows.length;  
                var resultArray = [];  
                resultArray = listData.ResultTables[0].ResultRows;  
                debugger;  
                var other = {},  
                     letter,  
                     i;  
                for (i=0; i < resultArray.length; i++) {  
                     ln = ctx.ListData.ResultTables[0].ResultRows[i].SitePath.toString().toLowerCase().search("/dispform.aspx");  
                     listURL = ctx.ListData.ResultTables[0].ResultRows[i].SitePath.toString().substring(0,ln)  
                  // if other doesn't already have a property for the current letter  
                  // create it and assign it to a new empty array  
                  if (!(listURL in other))  
                   other[listURL] = [];  
                       other[listURL].push(resultArray[i]);  
                }  
                for(var propt in other){  
                  console.log(propt + ': ' + other[propt].length);  
                }  
           debugger;  
           _#-->  
     <tr id="_#= encodedId =#_" class="border_bottom ms-itmHoverEnabled" data-listitemid="_#= ctx.CurrentItem.ListItemID =#_">  
       <td>_#= siteTitle =#_</td>  
       <td><a href="_#= listURL =#_">_#= listTitle =#_</a></td>  
                <td align="center">_#= resultcount =#_</td>  
     </tr>  
   </div>  
 </body>  
 </html>  

Step 2: Add a CSS file:

I cheated a little bit here and just borrowed some nice table CSS from the very useful DataTables JQuery plug-in.

First, ensure the Control File references this "custom css" file. See above for details.

Within this custom.css file, include the following classes:

 /* Table styles */  
 table.dataTable {  
      width: 100%;  
      margin: 0 auto;  
      clear: both;  
      border-collapse: separate;  
      border-spacing: 0;  
 }  
  /* Header and footer styles: */  
 table.dataTable thead th,  
 table.dataTable tfoot th {  
      font-weight: bold;  
 }  
 table.dataTable thead th,  
 table.dataTable thead td {  
      padding: 8px 10px;  
      border-bottom: 1px solid #111111;  
 }  
  /* Body styles: */  
 table.dataTable tbody th,  
 table.dataTable tbody td {  
      padding: 8px 10px;  
 }  
 table.dataTable th.center,  
 table.dataTable td.center,  
 table.dataTable td.dataTables_empty {  
      text-align: center;  
 }  
 table.dataTable th.right,  
 table.dataTable td.right {  
      text-align: right;  
 }  

Again, in case I missed anything, you can download working versions of this code from GitHub.


Step 3: Add the Content Search Web Part to your page and configure it

Add the Content Search Web Part to your page: Edit Page > Insert > Web Part > Content Rollup > Content Search (just so we're clear we're talking about the same thing)

and configure with the following properties:

Change Query

In Advanced Mode, update the Search Query to read:
ContentTypeId:0x0108* (IsDocument:"True" OR contentclass:"STS_ListItem") AssignedTo={User.Name} PercentCompleteOWSNMBR<>1
Basically what this is saying is: get all items (IsDocument:"True" OR contentclass:"STS_ListItem") of Content Type Task (ContentTypeId:0x0108), and all child content types (*), that are assigned to the current user (AssignedTo={User.Name}) and that are NOT 100% Complete (PercentCompleteOWSNMBR<>1).
Note that no 'path' variable is defined, so it will return content from all Site Collections on the tenant (that the current user has permissions to). This could be updated to include just the current site collection if that was your requirement. Just add:  path:{SiteCollection.URL}

Another way to meet this requirement, without the need to include your filtering logic in each web part, would be to create a custom Result Source in the Search Administration and refer to that

Assign the Display Templates

Under the Display Templates properties, select the respective Display Templates that we created above:

Set the Content Search Webpart properties

THE CONCLUSION

Thanks for reading this blog! I assume if you got this far that the content has been of interest to you. As mentioned, what you see in this post isn't complete, so you may to look at GitHub for the full, working model.
Hope this has been of assistance!