Friday, March 9, 2018

Creating a New Hire Workflow...Using Tasks Created Dynamically from a SharePoint List

The following describes how to create a workflow that uses tasks that are updated in a regular SharePoint list, allowing your end users to create/edit/remove/assign tasks in a SharePoint list, so they never have to open up SharePoint Designer.

It's a process I like to use for a new hire process or for offboarding, or any situation where tasks tend to change regularly.

It essentially involves building:
  • New Hire list - this is the list your users will be filling out to kick off the new process, and contain all the information about the new hire
  • Task Administration list - Create a custom list that will store the tasks we want to be created when the workflow runs
  • Tasks list - that will store the tasks that will get created and assigned to users
  • Workflow - loops through each item and assigns the tasks to the users at runtime

and the process is:
  • Somebody fills out a form to add a new hire
  • This kicks off the workflow
  • The workflow loops through the Task Administration list, creating tasks in the Tasks list
  • Everyone assigned a task gets an email at the end

New Hire List List

The new hire list collects the information about the new hire, and kicks off the process.  


Task Administration List

The Task Administration list is just a custom list with the following fields: 
  • Title - this will be the name of the task that will get created, e.g. "Create AD Account"
  • Assigned To - this is the person or group the task will be assigned to
  • Task Type - [Task or Notification] - I use this to distinguish if I should create a task, or just send an email notification
  • Task Active For - [Employees & Non-Employees, Employees or Non-Employees] - this determines whether the task should get assigned for different categories of users


Tasks List

The Tasks list is just an OOB Task list:

Workflow

The workflow essentially just queries our Task Admin List, loops through each item, creates a task, and keeps a running list of who to email at the end (so everyone gets just one email no matter how many tasks are create for them).

In this example I also added two extra bits of functionality:
  • I added a "Task Active For" column in the Task Admin list, which specifies if a task is needed for Employees, Non-Employees, or both.  The workflow checks this and assigns tasks accordingly
  • I also added a "Task Type" column in the Task Admin list.  If marked as a Task, a task gets created.  If a Notification, it just sends an email to the users assigned, and no task is created.

Wednesday, February 7, 2018

Display all Sites and Documents a user can access in SharePoint

Summary


This post walks you through the steps of creating a page that will display every shared document, folder and site that's been shared with a user, avoiding the reliance on email links when items are shared.  The end result looks something like below.



Solution


Search can bring back everything a user has access to.  So by modifying the Search Content Web Part, I brought back all files, folders and sites.  I updated the Display Templates to show the sites that the documents are in, and added a hover panel that has a document preview and shows the path where the document lives.

I added a Search Refinement Web Part, that displays all of the sites along with the item count under each site.  This way a user can easily drill down into different areas.

And lastly I added a search box so that users can perform a search on the results to aid in finding specific items.

Setting up the  web parts


Search Content Web Part


First I created a page and added a Search Content Web Part.  In order to bring back all files/folders/sites, so I changed the Query text to be:

Path:"https://vivity.sharepoint.com" -Path:"https://vivity.sharepoint.com/SiteAssets/" ContentTypeId:0x0120* -ProgID:OneNote.Notebook    -ContentTypeId:0x012002* -ContentTypeId:0x012000C0* -ContentTypeId:0x0120001928* ContentTypeId:0x010100* {QueryString.searchTerm}

Which searches my site, leaves out SiteAssets and OneNote Notebooks, and brings back derivatives of the contenttypes I want (files, folders and sites).  I am also searching for the querystring parameter "searchTerm", so a wildcard is thrown in the url as a default, which is replaced if a user submits a search.


I also mapped the ows_SiteName crawled property to RefineableString02 and am sorting by that.

Next, I wanted to update my Display Templates (/_catalogs/masterpage/Display Templates/Content Web Parts).  I started out with the Control_ListWithPaging.html and the Item_TwoLines.html and modified those.  

Control_ListWithPaging became Control_GroupedSites_WithHover, and I added some paging logic from here.  I also added a javascript file that sets some variables I'll be using, and adds some styling to my display template.

Item_TwoLines became Item_GroupedSites_WithHover, and my goal here was to display the documents, and added some code to make the Site Title be the header for every set of documents.

I also wanted more contextual information about each document, so I added a hover panel, which I updated to display the folder path and a document preview.

To show the hover panel, I started with the /_catalogs/masterpage/Display Templates/Item_Default_HoverPanel.html file, and merged it with the Item_Word_HoverPanel.html file, making some slight modifications to make it more generic and show the folder path so we have some context of where the document lives.  I named it Item_Default_HoverPanel_SharedDocuments.html.

Search Refiner Web Part


Additionally, users usually have a sense of where a document lives that they're searching for, so I added a Search Refinement web part that lists all of the sites that are returned an displays them so you can drill down easily.



and when you select a site, it will only bring back those items:


I updated the refiner templates so that they display in a table format, and you can find these updates in  /_catalogs/masterpage/Display Templates/Filters/Control_Refinement_SharedDocuments.html and Control_Refinement_SharedDocuments.html.

Search


Lastly I wanted a way for users to be able to search at any point, so I added a Content Query Web Part with some html/javascript that simply adds a querystring parameter and resubmits the page, and the the Content Search Web Part uses as a search term.

Code


All the code, templates and web parts are up on my github:  https://github.com/sregan1/SharePoint-Office365/tree/master/Shared%20Documents

Monday, January 29, 2018

Refresh Mapped SharePoint Folders & Fix the "Not Accessible" Error Message

PROBLEM

If you have mapped network drives to SharePoint document libraries, when accessing it from File Explorer you'll occasionally get the error:

[path to your folder] is not accessible.  You might not have permission to use this network resource.  Contact the administrator of this server to find out if you have access permissions.

Access Denied.  Before opening files in this location, you must first add the website to your trusted sites list, browse to the web site, and select the option to login automatically.
There doesn't seem to be any way you can make this completely go away, but by using some javascript to mimic the action, you can create an easy way for your end users to correct that problem.

SOLUTION

Our solution was to create a desktop shortcut, which users can double-click, that will reauthenticate the user and remedy the issue.

When clicked, an instance of IE will be opened, to a page where we will mimic the "Open with Explorer" functionality, and ultimately the File Explorer will be displayed.

So let's get started.

If you create a page on your site (e.g. OpenWithExplorer.aspx), add a content web part, and in the html add the following code:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
function getParameterByName(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

$(document).ready(function() {
// This is the default url to open if no parameter is passed
var url = 'https://vivity.sharepoint.com/Documents';

// See if there is a url parameter, if so grab it.
var qsUrl = getParameterByName("url", window.location.href);

// If it exists, change the default url
if (qsUrl != null)
{
url = qsUrl;
}

// Open up Explorer with our url
CoreInvoke('NavigateHttpFolder', url, '_blank');
});
</script>

Then create a desktop shortcut (right-click on the Desktop, select New -> Shortcut), and set the target to:

"C:\Program Files\Internet Explorer\iexplore.exe" https://vivity.sharepoint.com/Pages/OpenWithExplorer.aspx?url=https://vivity.sharepoint.com/sites/HR/Shared%20Documents

with the url parameter the path to the folder you want to open.

and set the Start in to:  "C:\Program Files\Internet Explorer", and optionally change the icon.


You'll then have an icon on your desktop, which when you double-click it will:

      1) Open up an instance of IE
      2) Open up the library in File Explorer


Or you can just as easily create a powershell script and run it with a hidden IE window:

$ie = new-object -com "InternetExplorer.Application"
$ie.navigate("https://vivity.sharepoint.com/Pages/OpenWithExplorer.aspx")

Thursday, December 14, 2017

List all Files & Folders for External Users (and Internal Users too)

This happens regularly with external users, and sometimes with internal users:  Someone shares a document or a folder with you, and you receive an email link to that item, and unless you bookmark that location, you're constantly referring to the email for the link.  Now when you're shared many files and folders, this can quickly get out of hand.

So a quick solution for this is to make a page that users can go to, that will display all of the folders and documents they have access to, like below:


This is just a page with two Search Content web parts, and a new Display Template.  For the Display Template:


  • First make a copy of the Item_TwoLines.html file on the site collection you're in, and name it something else, e.g. Item_UserAccess.html.
  • Change the title to "User Acces":  
<title>User Access</title>
  • Replace the mso:ManagedPropertyMapping line with the following:
<mso:ManagedPropertyMapping msdt:dt="string">&#39;Link URL&#39;{Link URL}:&#39;Path&#39;,&#39;Line 1&#39;{Line 1}:&#39;Title&#39;,&#39;Line 2&#39;{Line 2}:&#39;Title&#39;</mso:ManagedPropertyMapping>

  • You can then update the content in the display template by replacing the cbs-item div with the following:

        <div class="cbs-Item" id="_#= containerId =#_" data-displaytemplate="Item2Lines">
            <div class="cbs-Detail" id="_#= dataContainerId =#_">
                <h3>_#= line1 =#_</h3>
<!--#_
if(!line2.isEmpty)
{
_#-->
                <div class="cbs-Line2 ms-noWrap" title="_#= $htmlEncode(line2.defaultValueRenderer(line2)) =#_" id="_#= line2Id =#_">
                 <a class="cbs-Line1Link ms-noWrap ms-displayBlock" href="_#= linkURL =#_" title="_#= $htmlEncode(line1.defaultValueRenderer(line1)) =#_" id="_#= line1LinkId =#_">_#= line2 =#_</a>
                </div>
<!--#_
}
_#-->
                </div>
        </div>

  • Once these changes are made, save your file.
  • Then go to Site Settings -> Master pages and page layouts -> Display Templates -> Content Web Parts -> Item_UserAccess.html and select "Publish a Major Version"

Next, add a Search Content web part, change the query text to:

For Folders
Path:"https://yoursite.sharepoint.com/teams/teamsites/" -Path:"https://yoursite.sharepoint.com/teams/teamsites/SiteAssets/" ContentTypeId:0x0120* -ProgID:OneNote.Notebook    -ContentTypeId:0x012002* -ContentTypeId:0x012000C0* -ContentTypeId:0x0120001928*

Where the ContentIDs above are bringing back folders, excluding OneNote Notebooks and MicroFeed folders.  And we're excluding SiteAssets which generally most everyone has access to.

For Documents
Path:"https://yoursite.sharepoint.com/teams/teamsites/" -Path:"https://yoursite.sharepoint.com/teams/teamsites/SiteAssets/"  ContentTypeId:0x010100*

Next update the items to show, change the Display Template to "List with Paging" and your new display template "User Acces":


And that should be it!

Thursday, December 7, 2017

SharePoint Designer Workflow suspended when you have a blank lookup field

I've noticed an issue when you're using a SharePoint Designer workflow, and you use the Current Item:LookupField in a task or an email, and that lookup value is blank, and you're setting it to use Lookup Values, Comma Delimited



You'll get an error of something like: 

System.InvalidOperationException: Values must be bound to a non-null expression before ForEach activity 'ForEach<DynamicValue>' can be used.

If you set it as a string, you'll see the JSON that's coming back, and for empty values it returns the string: 

{"results":null}

Since Designer can't seem to handle that null object, for any lookup values you can do the following:

1)  Save the value as type String
2)  Test for the empty/null case by testing for the {"results":null} string
3)  Then set the value accordingly

So in my case, I've setup my lookup values as such: 


Where in Step 1 I'm using Current Item:Site Access As String, and inside the if statement I'm using the Current Item:Site Access as Lookup Values, Comma Delimited.

Friday, September 29, 2017

Azure AD: Get All User Properties / Attributes

To get all of the Azure AD user properties, you can add a format-list at the end and that should do the trick:

Get-msoluser -UserPrincipalName your.user@yourdomain.com | FL


Wednesday, September 13, 2017

Set and Unset Multi-Select Lookup field values in SharePoint Online

If you ever need an event to set the values of a multi-select lookup field in SharePoint, you can use the following:

function SetLookup() 
{
// define the items to add to the results (i.e already selected) this the visual part only   
var $resultOptions = "<option title='MyValue' value='2'>MyValue</option>";   
// this is the list of initial items (matching the ones above) that are used when the item is saved.  '|t' is the divider 
var $resultSpOptions = "2|tMyValue";   
 
// remove the option selected.  NOTE: These are in alphabetical order and thus this ID may differ from the select value   
//$("[id$='_SelectCandidate'] option:eq(1)").remove();  
$("[id$='_SelectCandidate'] option[value='2']").remove();

// append the new options to our results (this updates the display only of the second list box)   
$("[id$='_SelectResult']").append($resultOptions);  
// append the new options to our hidden field (this sets the values into the list item when saving)   
$("[id$='MultiLookup']").val($resultSpOptions);   
}

function UnsetLookup() 
{
// define the items to add to the results (i.e already selected) this the visual part only   
var $resultOptions = "<option title='MyValue' value='2'>MyValue</option>";   
// this is the list of initial items (matching the ones above) that are used when the item is saved.  '|t' is the divider 
var $resultSpOptions = "2|tMyValue";   
 
// remove the option selected.  NOTE: These are in alphabetical order and thus this ID may differ from the select value   
$("[id$='_SelectResult'] option:eq(1)").remove();  
$("[id$='_SelectResult'] option[value='2']").remove();  

// append the new options to our results (this updates the display only of the second list box)   
$("[id$='_SelectCandidate']").append($resultOptions);  

// append the new options to our hidden field (this sets the values into the list item when saving)   
var value = $("[id$='MultiLookup']").val();
value = value.replace($resultSpOptions, "");

//alert('value: ' + value);
$("[id$='MultiLookup']").val($resultSpOptions);   
}


//Execute the Query.
$(document).ready(function(){

// In this case, we're triggering by a dropdown named DropdownToTrigger
$('select[title="DropdownToTrigger"]').on('change', function() {
if ($(this).val()=='Something') 
{
SetLookup();
   } 
   else
   {
UnsetLookup();
   }
});


});