You may also be interested in: Creating insightful dashboards in SharePoint - Collabion Charts for SharePoint
Editor’s note: Contributor Craig Pilkenton is a Senior Microsoft Consultant for Slalom Consulting.
Summary
In working on SharePoint enhancement projects for clients, I’m eventually asked "How can I save my searches in SharePoint?". After getting ‘that look’ when mentioning how they could use the Alerts and RSS Feed features to have new items pushed to their Inbox, I start into a wistful spiel of how the ‘My Links’ feature used to be available in SharePoint 2007’s Site Actions dropdown, hooked directly to the My Sites for storing and accessing them. I then finish the tale with the sad ending that the link was removed from Site Actions in SharePoint 2010 and only available by going directly to your My Site (if even provisioned!).
While the feature is available in a users My Site, many companies still aren’t giving out those sites to their employees, or even creating that Web Application for us to utilize with a custom Site Action link that could load the hidden Application Page. So what’s a developer to do? That’s right, create a solution with JavaScript/jQuery!
Body
I recently had to come up with such a solution where it all had be client-side code for future migration to SharePoint 2013. Based on their requirements, it needed to have a small visual footprint on the Results page, launch an existing search or quickly save the current search with a custom name tied to the individual saving it, and be able to get to a "My Searches" List on the Global Navigation Bar for launching, editing, or deleting those saved searches. A later feature was added so that when dropped on the Advanced Search Page it would fill in previously chosen values back to their appropriate filter boxes, something missing in normal SharePoint Search.

1) App overview
What to Save?
When doing a search in SharePoint, we type a keyword into whichever search textbox is being used and after a few seconds our Results page appears with the items most relevant from the Index and some Refiners down the left side. If you look at the URL that was returned for the Results page, you will notice something like this: ("…/Results.aspx?k=workflow"). SharePoint takes our keyword(s) and posts them to the Results page using the QueryString variable called "k=", filling in that pages search textbox with our original query for consistency. Since SharePoint Search is URL-driven, meaning that whatever is after that QueryString variable are the results returned from the index, all we have to do is save that URL to a Custom List for easy access and post it for the user when they want to re-run the search

2) SharePoint Search URL’s
Where to Save?
So for designing how to save these searches, I started first with the "data layer". For my Custom List to store’s the Search Results URL’s, what columns do I need to create. That way it is easily manageable and accessible. I needed a Hyperlink column for the URL, a Single Line of Text column for the custom title, and a Person or Group column to create a view for only their links. For a nicer-looking link in the List View, I also added a Calculated Column to combine the URL with the custom title that included Path to SharePoint’s "Show Calculated Column HTML" tool.


3) My Searches Custom List
How to Save?
Now that we have a place to save the searches in our Site Collection we can start building out our code. Lines 1-25 are the necessary script includes and HTML for the requirements. Starting on line 27-36 are the variables I need to set for future use, including a reference to the hidden User Information List using REST on line 31 we’ll need to link the Search Result to the current user. Lines 38-41 are the $(document).ready() section with the 2 main setup functions running on each page load, getSPuser() & getDropdownData().
Since we can’t directly get the logged-in username, Lines 42- 64 grab the Site Collection ID assigned to our login that SharePoint stores in a special DOM element (Line 42), then queries the User Information List for our NT Logon, along with any other data we’d like. While we’ll just be saving the Site Collection ID into our My Searches List to hook the query to the user, we may need nice names and emails later on.
To get started saving this Search, the user clicks on the right arrows (Image 1) which fires activateSave() (lines 108-121 below) to toggle CSS classes for hiding and unhiding the name textbox and save icon controls. This way the user just sees the saved searches dropdown until they want to save the current Search Result. The code also pre-populates the name textbox with the search keyword for faster saving.
setSave(), which starts on line 65-90, is hooked to the image button from line 21 and fires from the OnClick() event gathering the Title, URL, and SharePoint User ID into a JSON object that is then posted to the My Searches List REST feed using the $.ajax() method (Lines 76-89). This Post saves the data back to the SharePoint List and on success checks the list for new saved searches by user, the getDropdownData() function detailed next, which we just created and then fires activateSave() to toggle the controls again back to their hidden state.
Show the Saved Searches!
With our code in-place to save our searches, we can now focus on the getDropdownData() function (Lines 91-107) which is called as mentioned above during page loads and on success of our save to show searches we already have saved by user. On Line 92 we create a standard REST URL (e.g. "http://myfarm.com/Search/_vti_bin/listdata.svc/MySearches?$filter=(SearchUserId eq xx)&$orderby=Title", etc.) against our My Searches List, limited by the current SharePoint user Id, and use the $.getJSON() command to return JSON data, looping through the results and adding the name & URL’s to our dropdown of saved searches with the Search Result URL as the dropdown item value’s.
Lines 108-121 contain the activateSave() function mentioned above for toggling CSS classes to hide or unhide controls. The fireSavedSearch() function on Lines 122-127 is hooked to the Searches dropdown onclick event (Image 1, Line 10 HTML) and grabs the currently selected value, then setting the browsers window.location equal to that URL. For the setupControls() function located on Lines 128-142, the code is evaluating whether this is a Search Results page to allow the saving functionality, or to just show the My Searches dropdown. This way the Web Part can be added to any page for showing saved searches, but only allow saving from a page that will have our "k=" QueryString variable. This function was later enhanced to evaluate if added to the Advanced Search page (Lines 135-137) and if so, fire the setupAdvancedSearch() function and its children functions detailed below to fill in previously chosen values back to their appropriate filter boxes.
Finally, Lines 143-263 are all the functions necessary to parse the different QueryString variables into the correct textbox or dropdown controls to enhance the Advance Search Page. While not required for the My Saved Searches feature, it helped increase the usefulness of the saved searches by putting the chosen metadata back where it was created.
<script src="../../SiteAssets/JavaScript/jquery-1.9.1.min.js"></script>
<script src="../../../SiteAssets/JavaScript/gRED_Overrides.js"></script>
<link rel="stylesheet" href="/Search/SiteAssets/Scripts/MySearches_Toolbox.css" />
<div id='selectionarea' class='rounded-corners'>
<table class="csTableFilters">
<tr>
<td class="csTdFilters">
<label id="lblMessages"> </label>
<SELECT id="ddlFilteMySearches" class="csFilterDropdowns" onclick="fireSavedSearch()">
</SELECT>
</td>
<td class="csTdFilters">
<INPUT TYPE="BUTTON" VALUE="" ONCLICK="activateSave()" id="btnActivateSave" class="csSearchesBtn">
</br><label id="lblActivateSave" class="csSearchesLabel">Save Search</label>
</td>
<td class="csTdFilters">
<input type="text" id="txtSearchTitle" class="csTxtFilter hideControl" maxlength="50" />
</td>
<td class="csTdFilters">
<INPUT TYPE="BUTTON" VALUE="" ONCLICK="setSave()" id="btnSetSave" class="csSearchesBtn hideControl">
</td>
</tr>
</table>
</div>
<script type="text/javascript">
var currUrl = window.location.href.toString();
var rootSiteUrl = currUrl.split("Search")[0];
var dataSvc = "_vti_bin/listdata.svc/";
var searchSite = "Search/";
var listUserInfo = "UserInformationList?$filter=(Id eq xx)";
var listMySearches = "MySearches?$filter=(SearchUserId eq xx)&$orderby=Title";
var countMySearches = 0;
var spUserId = "";
var spUserAccount = "";
var spUserName = "";
$(document).ready(function() {
getSPuser();
getDropdownData();
});
function getSPuser() {
spUserId = _spPageContextInfo.userId;
getSPuserById(spUserId);
}
function getSPuserById(userId) {
var strRestFullPath = rootSiteUrl + dataSvc + listUserInfo.replace('xx',userId.toString());
var foundName = "";
$.getJSON(strRestFullPath,function(data) {
$.each(data.d.results, function(i,result) {
var arrNodes = JSON.stringify(result).split(':');
for (var nde in arrNodes) {
var currNode = arrNodes[nde];
if(currNode.indexOf("Account") > -1) {
spUserName = currNode.toString().split(',')[0].replace(/"/g, "");
}
if(currNode.indexOf("WorkEMail") > -1) {
spUserAccount = currNode.toString().split(',')[0].replace(/"/g, "");
}
}
});
});
}
function setSave() {
var newSearch = {
Title: $("#txtSearchTitle").val().trim(),
SearchUrl: currUrl.toString(),
SearchUserId: spUserId,
AdvancedParamaters: currUrl.split('?')[1]
};
var body = JSON.stringify(newSearch);
var postUrl = rootSiteUrl + searchSite + dataSvc + listMySearches.split('?')[0];
console.log(postUrl);
console.log(body);
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
processData: false,
url: postUrl,
data: body,
dataType: "json",
success: function () {
$("#lblMessages").html(" - <font color='black'>Search saved</font>");
getDropdownData();
activateSave();
},
error: function (xhr, status, error) { $("#lblMessages").html(" - <font color='red'>Error: " + xhr.responseText + "</font>"); }
});
}
function getDropdownData() {
var restTarget = rootSiteUrl + searchSite + dataSvc + listMySearches.replace('xx',spUserId.toString());
var elem = $("#ddlFilteMySearches");
elem.empty();
$.getJSON(restTarget,function(data) {
$.each(data.d.results, function(i,result) {
var strItem = result.Title.toString();
var strVal = result.SearchUrl.toString();
$('<option />', {value: strVal, text: strItem}).appendTo(elem);
countMySearches++;
});
});
setupControls();
elem = $("#ddlFilteMySearches");
elem.prepend("<option>Select to fire a Search...</option>");
}
function activateSave() {
if($('#btnSetSave').hasClass('hideControl')) {
$('#lblFilterSearchTitle').removeClass('hideControl').addClass('showControl');
$('#txtSearchTitle').removeClass('hideControl').addClass('showControl');
$('#btnSetSave').removeClass('hideControl').addClass('showControl');
$("#lblActivateSave").text("Undo Save");
}
else {
$('#lblFilterSearchTitle').removeClass('showControl').addClass('hideControl');
$('#txtSearchTitle').removeClass('showControl').addClass('hideControl');
$('#btnSetSave').removeClass('showControl').addClass('hideControl');
$("#lblActivateSave").text("Save Search");
}
}
function fireSavedSearch() {
var ddlVal = $("#ddlFilteMySearches");
if(ddlVal.get(0).selectedIndex != 0) {
}
}
function setupControls() {
if(currUrl.toLowerCase().indexOf("/search/pages/results.aspx") == -1) {
$('#btnActivateSave').removeClass('showControl').addClass('hideControl');
$('#lblActivateSave').removeClass('showControl').addClass('hideControl');
$('.csTableFilters').css('margin-top','10px').css('margin-bottom','-0px').css('margin-right','-100px');
$('#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036').css('margin-top','-40px');
}
if(currUrl.toLowerCase().indexOf("/search/pages/advanced.aspx") > -1) {
setupAdvancedSearch();
}
$("#txtSearchTitle").val($("#ctl00_m_g_b4ca66bd_4c70_41c3_acff_1f8e44679522_SE86CCFBC_InputKeywords").val());
var setupLink = "<u><a href='"+rootSiteUrl + searchSite + "Lists/" + listMySearches.split('?')[0]+"' alt='My Searches'>";
setupLink += "My Searches</a></u>";
$(".csDivHeaderMsg").html(setupLink);
}
function setupAdvancedSearch() {
if(currUrl.indexOf("?") > -1) {
var arrQueryParams = currUrl.split('?k=')[1].split(' ');
var mainDdlProp = $("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_PS_plb_0");
var firstTxtProp = $("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_PS_pvtb_0");
mainDdlProp.change(function () {
var blCreateDdl = false;
switch($(this).val()) {
case "gredprojectname":
case "gredprotocolid":
case "gredfunctionalareaname":
blCreateDdl = true;
break;
}
if(blCreateDdl) {setupKeyDropdown(firstTxtProp,$(this).val(),""); }
else { removeKeyDropdown($("#ddlProperty_0"),firstTxtProp); }
});
for(var param in arrQueryParams) {
var paramVal = arrQueryParams[param];
var formVal = "";
if(paramVal.indexOf("ALL") > -1) {
formVal = fixAsciiToChar(fixAsciiToChar(paramVal,"("),")").split('(')[1].replace(')','');
$("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_TQS_AndQ_tb").val(formVal);
}
if(paramVal.indexOf(""") > -1) {
formVal = fixAsciiToChar(paramVal,""");
$("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_TQS_PhraseQ_tb").val(formVal);
}
if(paramVal.indexOf("ANY") > -1) {
formVal = fixAsciiToChar(fixAsciiToChar(paramVal,"("),")").split('(')[1].replace(')','');
$("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_TQS_OrQ_tb").val(formVal);
}
if(paramVal.indexOf("NONE") > -1) {
formVal = fixAsciiToChar(fixAsciiToChar(paramVal,"("),")").split('(')[1].replace(')','');
$("#ctl00_m_g_3982de3b_d4aa_46fc_a672_9a8f33c63036_ASB_TQS_NotQ_tb").val(formVal);
}
if(paramVal.indexOf(":") >-1) {
formVal = fixAsciiToChar(paramVal,":").split(':');
mainDdlProp.val(formVal[0]);
setupKeyDropdown(firstTxtProp,formVal[0],fixAsciiToChar(formVal[1],"-"));
}
}
}
}
function setupKeyDropdown(cntrlInsertBefore,strKey,strDefaultVal) {
$("#ddlProperty_0").remove();
var keywordDropdown = $('<select/>')
.attr('id', 'ddlProperty_0')
.attr('class', 'ms-advsrchPropertyDDL csFilterDropdowns')
.change(function () {
cntrlInsertBefore.val($(this).val());
});
setupDropdowns(keywordDropdown,strKey,strDefaultVal);
keywordDropdown.insertBefore(cntrlInsertBefore);
cntrlInsertBefore.val(strDefaultVal).attr('disabled','true');
}
function removeKeyDropdown(cntrlRemove,cntrlToEnable) {
cntrlRemove.remove();
cntrlToEnable.val("").removeAttr('disabled');
}
function setupDropdowns(ddlElem,strKey,strDefaultVal) {
var listTarget = "";
switch(strKey) {
case "gredprojectname":
listTarget = "GREDListProjectName";
break;
case "gredprotocolid":
listTarget = "GREDListProtocolID";
break;
case "gredfunctionalareaname":
listTarget = "GREDListFunctionalAreaName";
break;
}
getDropData(listTarget,ddlElem,strDefaultVal);
}
function getDropData(listTarget,targetDropdown,strDefaultVal) {
var restTarget = rootSiteUrl + dataSvc + listTarget;
$(targetDropdown).append("<option> </option>");
$.getJSON(restTarget,function(data) {
$.each(data.d.results, function(i,result) {
var strItem = result.Value.toString();
targetDropdown.append("<option>"+strItem.trim()+"</option>");
});
targetDropdown.val(strDefaultVal);
});
}
function fixAsciiToChar(fullString, asciiTarget) {
var newString = fullString;
var counter = 1;
if(fullString.indexOf(asciiTarget) > -1) {
var asciiReplacer = "";
switch(asciiTarget) {
case '(':
asciiReplacer = "(";
break;
case ')':
asciiReplacer = ")";
break;
case ':':
asciiReplacer = ":";
break;
case '-':
asciiReplacer = "-";
break;
case ':':
asciiReplacer = ":";
break;
case '"':
counter++;
asciiReplacer = "";
break;
}
for(var aa = 0; aa < counter; aa++) {
fullString= fullString.replace(asciiTarget,asciiReplacer);
}
newString = fullString;
}
return newString;
}
</script>
Summary
Although I’ve built out a solution using server-side code to put back the ‘missing’ Site Actions link for popping the My Links Application Page, without the availability of My Sites to store those links, creating a custom client-side solution was the only way to deliver this Search enhancement. The end-result is a Web Part that can be dropped on any page for quick access to their saved searches while having a small UI profile and doesn’t require navigating away from the core idea; seeing their favorite Search Results.
Reference Links