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();
   }
});


});

Monday, August 14, 2017

SharePoint Online: Set the default value for a Lookup (SPFieldLookupMulti) column

The following code will set the value for a Lookup field in SharePoint, where the highlighted fields are what you'd need to update.

           // define the items to add to the results (i.e already selected) this the visual part only  
           var $resultOptions = "<option title='Veeva Vault' value='3'>Veeva Vault</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 = "3|Veeva Vault";  
            
           // remove the option selected on the left side.  NOTE: These are in alphabetical order and thus this ID will probably differ from the select ID (and is 0 based)
           $("[id$='_SelectCandidate'] option:eq(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);   

You can get the values by using the developer tools (e.g. pressing F12 in Chrome) and inspecting the picker element:



you'll see:


where targeting 3 would result in the code above.

Shout out to this blog post which helped, just needed some minor tweaks to work in SharePoint Online.

Wednesday, August 9, 2017

Setting the default value for a Rich Text Field in SharePoint Online

To set the default value of a Multiple Lines of Text aka Rich Text Field aka SPFieldNote you can use the following JavaScript:

<script type="text/javascript">
$(document).ready(function () {

var defaultText = "Your html here";

                $('nobr:contains("[Your Field Title]")').closest('tr').find('div.ms-rtestate-write').html(defaultText);

});
</script>

Monday, August 7, 2017

Show the List Title in form views (NewForm.aspx, EditForm.aspx, DispForm.aspx)

Whenever the ribbon is being shown the title of the list will not be displayed:


So the user might not have any context to where they are.  So to add the Title of the list, use the following css into the PlaceHolderPageTitleInTitleArea content placeholder for NewForm.aspx, EditForm.aspx and DispForm.aspx pages in SharePoint Designer:

<style> 
    #s4-titlerow { 
display: block !important; 
    } 
</style> 

into:



And then you'll have a title in your form:






Get a user from a Person field and check if they are in a SharePoint group

If you have a Person field (people picker) in your SharePoint form, and need to get that value, check if they're in a group, and take an action based on that, you can use the following:

<script type="text/javascript">

function IsCurrentUserMemberOfGroup(groupName, pickerTitle, OnComplete) {

// get people picker
var peoplePicker = SPClientPeoplePicker.SPClientPeoplePickerDict[$('[title="' + pickerTitle + '"]')[0].id];

// get the user dictionary
    var users = peoplePicker.GetAllUserInfo();
   
    // get the user name
   var currentUserAccountName = users[0]['Key'];
       
      // get current context and web
      var currentContext = new SP.ClientContext.get_current();
        var currentWeb = currentContext.get_web();
                
        // get the user object
        var user = currentWeb.ensureUser(currentUserAccountName);
         
        // execute call to get user id
        currentContext.load(user);
    currentContext.executeQueryAsync(
        Function.createDelegate(null, OnUserSuccess), 
        Function.createDelegate(null, OnUserFailure)
   );

var groupUsers;

// Once we have the user, check if they're in the group
function OnUserSuccess(sender, args) {

// get all groups
var allGroups = currentWeb.get_siteGroups();
       currentContext.load(allGroups);

// get the group we're looking for
       var group = allGroups.getByName(groupName);
       currentContext.load(group);

// get the users of the group
       groupUsers = group.get_users();
       currentContext.load(groupUsers);

       currentContext.executeQueryAsync(OnGroupSuccess,OnGroupFailure);
}

// loop through all users in the group and look for our user
        function OnGroupSuccess(sender, args) {
            var userInGroup = false;
            var groupUserEnumerator = groupUsers.getEnumerator();
            while (groupUserEnumerator.moveNext()) {
                var groupUser = groupUserEnumerator.get_current();
                if (groupUser.get_id() == user.get_id()) {
                    userInGroup = true;
                    break;
                }
            }  
            OnComplete(userInGroup);
        }
        
        function OnUserFailure(sender, args) {
OnComplete(false);
}


        function OnGroupFailure(sender, args) {
            OnComplete(false);
        }    
}

function IsCurrentUserHasContribPerms() 
{
  IsCurrentUserMemberOfGroup("Leadership Team", "Nominee", function (isCurrentUserInGroup) {
    if(isCurrentUserInGroup)
    {
        alert('Leadership Team Members are not eligible to be nominated');
        $("input[value$='Save']").attr('disabled', true);
        $("span[id='Person']").css('color','red').css('font-weight','bold');
    }
    else {
    $("input[value$='Save']").attr('disabled', false); 
    $("span[id='Person']").css('color','black').css('font-weight','normal');
    }
  });
}


function SetOnChangeEvent() 
{
$('input[title="Nominee"]').blur(function(){
IsCurrentUserHasContribPerms();
});
}

$(document).ready(function(){ 

ExecuteOrDelayUntilScriptLoaded(SetOnChangeEvent, 'SP.js');

});
</script>

Monday, July 31, 2017

Have checkbox set and disable a metadata field

Here's a snippet of code you can throw into the NewForm.aspx page if you need a checkbox to set a value in a metadata field and disable that metadata field:

 //Execute the Query.
$(document).ready(function(){
// Get the value of the checkbox
if ($('input[name="NewDepartment"]:checked').val() == "true")
{
boolAutoApprove = true;
}
else 
{
boolAutoApprove = false;
}

// When checkbox is clicked, set or unset the value of the Department field
$("input[id ^= 'NewDepartment']").change(function(){
boolAutoApprove = !boolAutoApprove;

if (boolAutoApprove) 
{
$("div[title='Department']").children().prop("contenteditable",false).css('background-color','#E0E0E0');
$("div[title='Department']").parent().find('img').hide();
$("div[title='Department']").children().html("New Department");
}
else 
{
$("div[title='Department']").children().prop("contenteditable",true).css('background-color','#FFFFFF');
$("div[title='Department']").parent().find('img').show();
$(".ms-taxonomy-writeableregion").html("");
}
});

});

Thursday, July 20, 2017

Create a SharePoint View for All Tasks Assigned to Groups a User is in

I wanted to create a new SharePoint view for tasks that were assigned to me or any groups I'm in.  So first I went to the list where I want to create the view in SharePoint Designer and select new.  I named mine "My Groups":



Then right-click the view and select "Edit in Advanced Mode" to open it up, and search for:

 <Query/>


and replace it with the following:

<Query><Where>
<Or>
<Membership Type="CurrentUserGroups">
<FieldRef Name="AssignedTo"/>
</Membership>
<Eq>
<FieldRef Name="AssignedTo"></FieldRef>
<Value Type="Integer">
<UserID/>
</Value>
</Eq>
</Or></Where></Query>

Thanks goes out to Laura Rogers' article on this one!

Tuesday, July 18, 2017

Workflow hangs when calling a web service: "Invalid text value"

When I call a web service I'm usually spitting out what I get back by logging it to the history list.  And sometimes my workflow will hang with an error message along the lines of:

Retrying last request. Next attempt scheduled in less than one minute. Details of last request: HTTP InternalServerError to ...

Invalid text value.

A text field contains invalid data. Please check the value and try again. 

If you see this, it usually means you're logging something to the history list that's greater than 256 characters.

Wednesday, July 5, 2017

Auto Populate Person Field / People Picker in SharePoint Online

Wanted to throw some reference code out there for how to auto populate a Person field using jQuery and SPServices.  The full code is below, and I pasted this directly below the "PlaceHolderMain" in the NewForm.aspx file in SharePoint Designer.  You'll find the original article here.

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="/_layouts/15/clientforms.js"></script>
<script src="/_layouts/15/clientpeoplepicker.js"></script>
<script src="/_layouts/15/autofill.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2014.02/jquery.SPServices.js"></script>
<script type="text/javascript">

$(document).ready(function () {
SP.SOD.executeFunc('sp.js', null, function () {
retrieveUserInfo("Employee");  // Set this to the Title of the person column
    })
  });

  function retrieveUserInfo(PickerTitle) {
var currentuserName = $().SPServices.SPGetCurrentUser({
fieldName: "Title"
});

  $('input[title="' + PickerTitle + '"]').val(currentuserName).attr('size', 40);

    $('div[title="' + PickerTitle + '"] span.ms-helperText').hide();

    // Select the target user field from the dictionary of user fields on the page.
    var peoplePicker = SPClientPeoplePicker.SPClientPeoplePickerDict[$('[title="' + PickerTitle + '"]')[0].id];

    // Resolve the user using the value set in the input field.
    peoplePicker.AddUnresolvedUserFromEditor(true);
}

Tuesday, June 27, 2017

One-click approval of tasks in SharePoint Online

I was looking for a way to make it easier for users to approve SharePoint tasks, and settled on a great way to have an Approve and Reject link in the task email, which goes to a page I created that runs some JavaScript to approve/reject the task based on the parameters passed to it.  I've outlined how I did this below:

Step 1:  Create a page that will contain the JavaScript code

Create a page, I named mine ApproveReject.aspx, and add a content editor web part and put the following code into it:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript" src="_layouts/15/sp.runtime.js"></script>
<script type="text/javascript" src="_layouts/15/sp.js"></script>

<script type="text/javascript">
function getStringValue(strToParse, begin, end) {
 var strValue = "";
 var indexBegin = strToParse.indexOf(begin) + begin.length;

 strToParse = strToParse.substring(indexBegin);
 
 if (strToParse.indexOf(end) > 0) 
 {
 var indexEnd = strToParse.indexOf(end);  
 strValue = strToParse.substring(0, indexEnd);
 }
 else 
 {
  strValue = strToParse;
 }
   
 return strValue;
}

function getParameterByName(name) {
var 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, " "));
}
function onSuccess(){
 var name = getParameterByName('name');
 var outcome = getParameterByName('action');

 if (outcome == "Approved") 
 {
  document.getElementById("approve-reject-message").innerHTML = "<h2>Thanks for approving the Award for " + name + "</h2><br/><br/>You can now close this tab.";  
 }
 else 
 {
  document.getElementById("approve-reject-message").innerHTML = "<h2>The Award has been rejected for " + name + "</h2><br/><br/>You can now close this tab.";  
 }
}
function onError(sender, args) {
 document.getElementById("approve-reject-message").innerHTML = "Error:  " + args.get_message();
}

function approveTask(listTitle,itemId,action,success,error){
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle(listTitle);
var item = list.getItemById(itemId);
item.set_item('Checkmark',true);
item.set_item('PercentComplete',1);
item.set_item('Status',action);
item.set_item('TaskOutcome',action);
item.update();

ctx.executeQueryAsync(onSuccess, onError);
}
jQuery(document).ready(function ()
{
var taskUrl = getParameterByName('taskUrl');
var action = getParameterByName('action');
taskListName = getStringValue(taskUrl, 'Lists/', '/');
taskId = getStringValue(taskUrl, 'ID=', '&');    
    
   approveTask(taskListName,taskId,action,function(){
},
function(sender,args){
});
}); 
</script>
<div id="approve-reject-message">
</div>

Step #2:  Add links in your tasks to Approve or Reject

In the text of the task email, add a link for Approve and a link for Reject:

With the url as follows:

where the query parameters are as follows:

action:  the action we're taking (Approved/Rejected)
taskUrl:  a link to the url of the task
name:  the name to be displayed in the message that appears
mobile:  required to ensure the page gets displayed in the standard (non-mobile) view


Monday, May 29, 2017

Creating a SharePoint Mega Menu with CSS

Heather Solomon has a fantastic article on how to create a mega menu in SharePoint:


I started with this, and was looking to create a three column mega menu, which wound up looking like this:



The setup for the navigation should just be like one level deep:


And the css, which will need some modification based on your column needs is below:

/* NAVIGATION (TOP NAV BAR) - DROP DOWN MENU
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- */

/* Override jQueryUI.css */
#DeltaTopNavigation li.dynamic {
float: none !important;
}

/* Alter drop down menu placement */
.ms-core-listMenu-horizontalBox li.static.dynamic-children.hover > ul.dynamic,
.ms-core-listMenu-horizontalBox li.static.dynamic-children.hover-off > ul.dynamic{
top: 25px !important;  /* !important added to override inline SharePoint style */
left: -25px !important;  /* !important added to override inline SharePoint style */
}

/* Drop down menu container */
li.

li.static > ul.dynamic  {
width: 640px !important;  /* Needed to override generated inline style */
box-shadow: 0 0;
height: 365px !important;
height: auto;
padding: 0 0 1em;
}

/* Space out menu items and set parent positioning so child elements appear in the right location*/
li.static > ul.dynamic > li {
line-height: 1.8em;
position: relative;
}

/* Nav item formatting */
li.static > ul.dynamic > li.dynamic a,
li.static > ul.dynamic > li.dynamic a:link,
li.static > ul.dynamic > li.dynamic a:visited {
font-family: var(--font);
color: var(--sage);
font-size: 1.1em;
padding: 0 1em;
}
li.static > ul.dynamic > li.dynamic a:hover {
color: var(--sage);
background-color: var(--lightsage40);
}

/* Add "Sections" before nav items */
li.static > ul.dynamic > li:nth-child(1)::before {
content: "Corporate Services \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 Resources \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0 \00a0Go To";
text-transform: uppercase;
font-family: Lato, sans-serif;
color: #ccc;
padding: .6em 1em;
display: block;
border-bottom: 1px solid #ccc;
text-align: left;
width: 600px;
margin-bottom: 10px !important;
}

/* Shift nav items to second column starting with nav item #IndexOfColumn2 */
li.static > ul.dynamic > li:nth-child(n+9) {
margin-left: 16em;
top: -287px !important;
}

/* Shift nav items to third column starting with nav item #IndexOfColumn3 */
li.static > ul.dynamic > li:nth-child(n+18) {
margin-left: 32em;
top: -610px !important;
vertical-align: top;
}