Dynamic content with SharePoint Search and JavaScript
Sometimes, it can be difficult to provide users with content/information in a single overview. Especially with default SharePoint ‘click-and-go’ functionality.
I will describe two cases where a customer wanted to have content/information on a single page without having to spend much time on managing this page after they have been implemented.
Customer Case 1: Permanent Removal Checklist for archived dossiers
For a customer, I implemented a workflow driven RMS (Records Management System).
In short, all documents which belonged to a specific process were stored in a single dossier (document set) and after process closure, the dossier would be kept for a specific time before being permanently removed from SharePoint.
The customer wanted to have an overview of all the dossiers that would be permanently removed the next year.
All dossiers were (automatically) provided with a custom metadata field ‘Removal Date’ after being send to the repository. This field was being indexed by Search which means I could use it in a custom Search results page.
The customer preferred a single page which could be re-used every year. This could be done by simply re-configuring the search query every year, but I wanted that to be configured automatically (not because it’s too much work to edit your query once every year, but to explore the possibilities of dynamic Search pages).
I came up with the idea to work with JavaScript to get the current date, extract the year from it and to build up a search query to return all dossiers with ‘RemovalDate:nextYear’ and to add that query to the Search Results page URL.
I created a simple page with a Search Results webpart and a Content Editor webpart (to load the JavaScript). I then started to work on the JavaScript, which actually isn’t that difficult.
Since I’m not a developer it took me probably a bit more time than most developers and the coding could be a bit cleaner, so please don’t judge me on that 🙂
The first step is get the current date, count up to the next year and subtract the year from it:
var nextYear = new Date(); nextYear.setMonth(nextYear.getMonth() + 12); nextYear = nextYear.getFullYear();
The next step is to read the URL. We don’t want to get into an infinite loop where the URL keeps on changing after every refresh, so we only want to execute the script if the URL doesn’t already contain the query:
var url = windows.location.href; if (url.indexOf('wfVernietigingsdatumOWSDATE:') === -1){ if (url.indexOf('wfVernietigingsdatumOWSDATE:' + nextYear) === -1){ window.location.href = url + '?k=wfVernietigingsdatumOWSDATE:' + nextYear; } }
I also wanted to provide the user with information on which removal year is displayed. I did this by retracting the year from the query in the URL and adding this year to the webpart title, which is the ‘spanTitleText’ element of SharePoint:
else { var queryUrl = window.location.href; var query = queryUrl.split("?k=wfVernietigingsdatumOWSDATE:"); var year = query[1]; year = year.split("&"); year = year[0]; } document.getElementById("spanTitleText").innerHTML += ' ' + year;
After some visual modifications to the display template, the result of removal year 2031 (checklist year 2030) looks like this:
Customer Case 2: Distributors portal
Recently, a customer had a request to provide their (external) distributors with documentation on the extranet. Distributors were already given access to the extranet by using a 3rd party tool (Extranet Collaboration Manager for SharePoint 2013 by SharePoint Solutions), but weren’t allowed to browse the library where all of the documentation was stored, since there was also documentation in that same library for other distributors that they weren’t allowed to see. (We had turned off direct access to the library in case someone smart figured out the library URL, but documents were still reachable)
One possible solution to provide ‘easy access’ to the distributor’s documentation was to individually assign user permissions to each document which is – of course – a dirty solution you don’t want to provide your customer with.
Another possible solution was to create a library for each distributor and assign permissions to that library, which means all documents inside that library are only visible for that specific distributor. The problem was that some documents were available for multiple distributors which meant that all these documents had to be duplicated across multiple document libraries, which is also a dirty solution.
We soon realized we needed a better solution, but to achieve this we had to do more than just the ‘click-and-go’ functionality of SharePoint.
We then started to think about Search. There had to be a way to provide a logged in users with content, based on the user’s metadata…
Every document inside the Document library was tagged with a certain Product Code.
We came up with the idea to assign users of specific suppliers to certain product codes.
Since every supplier user has the same email address suffix (e.g. @supplierX.com), we wanted to map that suffix to certain product codes. We had to create a mapping list with the following metadata:
- Account (used to store the email address suffix)
- Product Code
We use ‘one on one’ mappings, which means we only mapped one account to one product code in a single record. We used this method because to didn’t want to perform string transformations in the JavaScript later on.
We now had a Document Library, with tagged documents and a mapping list. Now we had to find a way to connect both to each other.
The answer was to use a page with a JavaScript script which would read the user’s display name (for external users) or the user’s email address from the User Profile Service (for internal users) and trim this down to only use the suffix. (since we used a 3rd party tool for external sharing, external users didn’t have a user profile associated. In this case, the display name was the user’s email address)
We then could read the mapping list to find the assigned product codes and finally build up a link to a custom Search Results page with the query to search only for the assigned product codes.
a. The user’s display name or email address will be read and split at ‘@’ to provide only the email address suffix.
function execInitFlowOperation() { //Initialize and Flow context = new SP.ClientContext.get_current(); web = context.get_web(); //Start with loading user properties to set the users domain loadUserProperties(); } function loadUserProperties() { user = web.get_currentUser(); context.load(user); context.executeQueryAsync(onUserPropertiesSuccess, onUserPropertiesFail); } function onUserPropertiesSuccess(sender, args) { // For Extranet accounts the display name reflects the email address var displayname = user.get_title(); if(displayname.indexOf("@") > 0) { domain = "@" + displayname.split("@")[1]; } else { // for internal accounts the email method gets the email address from the user profile var email = user.get_email(); domain = "@" + email.split("@")[1]; } loadDistInfo(); }
b. The mapping list will be queried using a CAML query, which will search for the Account
c. The corresponding Product Code(s) will be returned
function loadDistInfo() { var distList = web.get_lists().getByTitle("MappingList"); var query = "" + domain + ""; var camlQuery = new SP.CamlQuery(); //camlQuery.ViewXml = ""; camlQuery.set_viewXml(query); distItems = distList.getItems(camlQuery); context.load(distItems, 'Include(ProductCode)'); context.executeQueryAsync(onLoadDistSucceeded, onLoadDistFailed); }
d. The link will be created with the corresponding query (RefinableString09: <productCode>). Multiple product code results will be separated by splitting them into ‘OR: RefinableString09: <productCode>’
function onLoadDistSucceeded(sender, args) { var currListItemCount = distItems.get_count(); var currItemEnumerator = distItems.getEnumerator(); var currItemDetails = ''; // Loop through all items while (currItemEnumerator.moveNext()) { // Get current item var currItem = currItemEnumerator.get_current(); if(currItemDetails == "") { currItemDetails = currItem.get_item("ProductCode"); } else { currItemDetails = currItemDetails + ";" + currItem.get_item("ProductCode"); } } //Create array from ProductCode string productCodes = currItemDetails.split(";"); //Build the Data Sheet Link using the ProductCode array var buildDataSheetLink = "/sites/distributors/SitePages/Datasheets.aspx?k="; for (var i = 0; i < productCodes.length; i++) { if (i > 0) { buildDataSheetLink = buildDataSheetLink + " OR "; } buildDataSheetLink = buildDataSheetLink + "RefinableString09:" + productCodes[i]; } // modify the link of the datasheets tile $("#datasheetslink").attr("href", buildDataSheetLink); }
Clicking the URL will provide the user with an overview of all the corresponding documents regarding the distributors account.
After some visual improvements, the result looks like this:
Clicking the Datasheets link, the following results will be loaded:
The query, built by the JavaScript was as follows:
https://something.something.com/sites/something/SitePages/Datasheets.aspx ?k=RefinableString09:0607 OR RefinableString09:0601 OR RefinableString09:0301 OR RefinableString09:0304 OR RefinableString09:0308 OR RefinableString09:1008……..
(and much more)
One last remark: Of course this solution does not take care of permissions on documents. It just helps showing relevant documents for an account. Search takes permissions on (content in) a library into account, but creating search based views does not prevent a user from accessing other content if he/she has the permissions to do so. For this particular customer case though, the solution implemented was exactly what they needed.