How to create SharePoint LinkedIn like user profile directory

A LinkedIn like experience for an employee profile can be simple as on the screens below. You can search for users by different criteria like skills, certificates, location, projects they have been involved in or more. Users should also be able to update their own profile.

SharePoint online page opened in browser
SharePoint online page opened in browser

Use the Search Service APIs to query users by name, title or email


If they are no custom user profile properties in the requirement, search for users can be as simple as executing piece of JavaScript to retrieve users by title, name email or phone.



fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/search/query?querytext='Velin*'&selectproperties='UserName,Title,JobTitle,WorkPhone'&sourceid='b09a7990-05ea-4af9-81ef-edfab16c4e31'&clienttype='PeopleResultsQuery'`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "GET"

}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);
})
})


The only thing that has to be replaced in this piece of code is querytext='Velin*' where Velin has to be replaced with dynamic input like querytext='<YOUR_SEARCH_KEYWORD>*'. The source id 'b09a7990-05ea-4af9-81ef-edfab16c4e31' tells SharePoint search to look for people only. We can also utilize the search api pagination options to load more pages with results if needed.


Use the User profile api to get more details about the user selected from the search results


Once the above code returns list users, the search result set also returns by default a property named AccountName. The AccountName value looks like this i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com and it can be used with the user profiles api to retrieve more details about the user that are publicly available.


// This property can be taken from the search results
AccountName = i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='${encodeURIComponent('i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com')}'`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "GET"
}).then((response) => {
if (!response.ok) {
reject(response);
}
return response.json();
}).then((response) => {
console.log(response);
})
})


The encodeURIComponent('i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com') can be replaced by the accountName of the selected user from the search results like encodeURIComponent('<ACCOUNT_NAME_OF_THE_SELECTED_USER>'). With that we will be able to retrieve additional information for user found from the users search directory.


Use user profile service rest APIs to retrieve the profile of the currently logged in user


Next, we can utilize the UPS apis to get the current user data from the SharePoint User Profiles and displayed when the user clicks on My Profile link.



fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
reject(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/SP.UserProfiles.PeopleManager/GetMyProperties`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "GET"
}).then((response) => {
if (!response.ok) {
reject(response);
}
return response.json();
}).then((response) => {
console.log(response);
})
})


There is one issue with that and the api will always return all to properties even they are 400. There is no way to select a specific set of properties. The APIs is responsive and event big set of data is not a big issue, but we pre-loaded that data on the background in our solution once user opens the search landing page.


Updating current user own profile sections


The user profiles api provides with a way to update user's own properties and depends of the user profile property this can be done in two different ways. If the property in the user profiles is of type SingleValueProfileProperty, usually used for information like users bio that consists of just text then a UPS SingleValueProfileProperties can be updated the following way. The body of the request requires accountName which is the accountName or the current user, propertyName that is the UPS property we are about to update and propertyValue which is a text of the new value to be set.



fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/SP.UserProfiles.PeopleManager/SetSingleValueProfileProperty`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "POST",
body: JSON.stringify({
'accountName': "i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com",
'propertyName': "finderCommunityOfPracticeBody1",
'propertyValue': "Velin Test gfdgf"
})
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();

}).then((response) => {
console.log(response);
})
})


The UPS property can also be of type MultiValuedProfileProperty. This is more suitable to information like skills where multiple values should be stored into the field. Then we can utilize one other endpoint of the user profiles service. The body of the request requires accountName which is the accountName or the current user, propertyName that is the UPS property we are about to update and propertyValues which is an array of new values to be set.



fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
this.handleResponseError(response);
reject(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/SP.UserProfiles.PeopleManager/SetMultiValuedProfileProperty`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "POST",
body: JSON.stringify({
'accountName': "i:0#.f|membership|velin.georgiev@mytenant.onmicrosoft.com",
'propertyName': "finderSkills",
'propertyValues': ["Technology1", "Written Communication2"]
})
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);
})
})


The difference here is that every value should be an item of array in the body.


Search by custom user profiles property


This is the tricky part. Imagine the stakeholders have requirement for new section named Certificates or something similar. Then we would have to introduce new custom User Profile Property to satisfy the ask.

This can be done form the Office 365 admin center, where we can access the classic features, go to user profiles and then go to manage properties. Create new property and make it visible to everyone, indexed as well as available for update from the user.


The next action in the setup is to make the new UPS properties that we can query, retrieve and use as refiner from the Search service apis. This will require to map the UPS fields into a RefinableString property or multiple properties depending on the requirements and whether additional refiners might be needed. The next screens show how we can go to the search schema, find RefinableString and map the user profiles crawled properties into it. The crawled user profile properties can be found under the people scope and they have the following format People:NAME_OF_UPS_CUSTOM_PROP.


Office 365 Admin portal opened in browser
Office 365 Admin portal opened in browser
Office 365 Admin portal opened in browser
Office 365 Admin portal opened in browser

What I did for that specific case is to jam all the custom UPS profile properties into one refillable managed property because my search criteria is that the user should be able to search for a keyword and if it is in one of those UPS properties then I display the user in the result. Depending on the requirements more than one RefinableString might be needed.


Once that setup is done and the content in the UPS properties is indexed by search then we can execute search query like on the example bellow:



fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/contextinfo`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose"
},
method: "POST"
}).then((response) => {
if (!response.ok) {
this.handleResponseError(response);
reject(response);
}
return response.json();
}).then((response) => {
console.log(response);

fetch(`https://mytenant.sharepoint.com/sites/finder-en/_api/search/postquery`, {
headers: {
"Accept": "application/json;odata=nometadata",
"Content-type": "application/json;odata=verbose",
"X-RequestDigest": response.FormDigestValue
},
method: "POST",
body: JSON.stringify({"request":{"__metadata":{"type":"Microsoft.Office.Server.Search.REST.SearchRequest"},"Querytext":"\"certbody1\" OR RefinableString199:\"certbody1*\"","RowLimit":10,"StartRow":0,"ClientType":"ContentSearchRegular","TrimDuplicates":false,"SelectProperties":{"results":["RefinableString199","RefinableString198","OriginalPath","Title"]},"SourceId":"b09a7990-05ea-4af9-81ef-edfab16c4e31","HitHighlightedProperties":{"results":[]},"Properties":{"results":[]},"RefinementFilters":{"results":[]},"ReorderingRules":{"results":[]},"SortList":{"results":[]}}})
}).then((response) => {
if (!response.ok) {
console.log(response);
}
return response.json();
}).then((response) => {
console.log(response);
})
})



JSON output

From that query can be noted that I am searching for "Querytext":"\"certbody1\" OR RefinableString199:\"certbody1*\"" and if that keyword 'certbody1' matches text in the RefinableString199 then I will get result back. RefinableString199 is the managed property where I jammed all the custom UPS properties.


Conclusion


Creating simple people search directory in SharePoint is a commonly asked requirement and with the help of classic features and the SharePoint Framework it is still doable to deliver cool looking LinkedIn like experience to the enterprise.


Links

  1. https://www.vrdmn.com/2013/07/sharepoint-2013-get-userprofile.html
  2. https://docs.microsoft.com/en-us/sharepoint/dev/general-development/work-with-user-profiles-in-sharepoint
  3. Let's see if Vesa will find this on the WeeklyDev. Click it Vesa