Service Portal search [ Item name + short description ], Please help

David Cross
Tera Expert

Hello All, I am new to SP and Angular. Can you please help me with a widget.
Here on search option i only get Item name, But instead we need Item name + short description

this is my Client code & below that is HTML code, Can you please suggest me what changes are required here to get Item name + short description

Service Portal Search :  [ We need Item name + Short description ]

find_real_file.png

 

Client Code:

 

function ($http, $filter, $location,spAriaUtil, $window, $scope, spAriaFocusManager, snAnalytics) {
var c = this;
c.options.glyph = c.options.glyph || 'search';
c.options.title = c.options.title || c.data.searchMsg;
c.options.color = c.options.color || "default";
c.searchTerm = c.data.q;
c.searchQuery = "";
c.pageID = $scope.page && $scope.page.id;
c.showSuggestions = c.data.searchTypeBehavior === "suggestions" && c.data.isSuggestionsEnabled === "true";
c.suggestionsLimit = c.options.limit > 0 ? c.options.limit : "";
c.latitude = null;
c.longitude = null;
c.isGlideSignalsLoaded = false;
c.isLocationTrackerDisabled = c.data.isLocationTrackerDisabled === "true";
c.sendAnalytics = function(type){
var payload= {};
payload.name = "Initiate Search";
payload.data = {};
payload.data["Keyword"] = (type == 'User Entered' ? c.searchTerm : c.searchQuery);
payload.data["Type"] = type;
payload.data["Page ID"] = c.pageID;
snAnalytics.addEvent(payload);
};

function initializeGlideSignals() {
if (!c.isLocationTrackerDisabled && window.GlideSignals.init) {
window.GlideSignals.init();
}
if (window.GlideSignals.trackEvent) {
c.isGlideSignalsLoaded = true;
}
}

if (window.GlideSignals)
initializeGlideSignals();

c.trackSuggestionsRenderedEvent = function(searchQueryLength, responseTimeInMilliSeconds){
if(c.isGlideSignalsLoaded)
GlideSignals.trackEvent('SEARCH_SUGGESTIONS_RENDERED', GlideSignals.priority.INFO,
{'applicationId': c.data.portalID,
'searchQueryLength': searchQueryLength,
'totalSuggestionsCount': c.totalSuggestionsCount,
'userHistorySuggestionsCount' : c.userHistorySuggestionsCount,
'instanceHistorySuggestionsCount' : c.instanceHistorySuggestionsCount,
'responseTime': responseTimeInMilliSeconds+' ms'
});
};

c.trackSearchClickedEvent = function(model){
if(c.isGlideSignalsLoaded) {
if(c.showSuggestions) {
GlideSignals.trackEvent('SEARCH_SUGGESTION_CLICKED', GlideSignals.priority.INFO,
{'applicationId': c.data.portalID,
'searchQueryLength' : c.searchQuery.length,
'suggestionClickedLength': model.name.length,
'totalSuggestionsCount': c.totalSuggestionsCount,
'suggestionClickedType': model.type,
'aggregatedClickIndex': getSearchItemIndex(c.searchItems, model),
'userHistorySuggestionsCount' : c.userHistorySuggestionsCount,
'instanceHistorySuggestionsCount' : c.instanceHistorySuggestionsCount,
'suggestionsDisplayLimit': c.suggestionsLimit,
'relativeClickIndex': getRelativeSearchItemIndex(c.searchItems, model)
})
}
else {
GlideSignals.trackEvent('SEARCH_TYPEAHEAD_CLICKED', GlideSignals.priority.INFO,
{'applicationId': c.data.portalID,
'searchQueryLength' : c.searchQuery.length,
'typeaheadClickedLength': model.name && model.name.length,
'resultSysId': model.sys_id,
'clickIndex': model.query_location != null ? model.query_location : getSearchItemIndex(c.searchItems, model),
'sourceId': model.type != null ? model.type : model.table,
'typeaheadDisplayLimit': c.options.limit
})
}
}
}

c.onSelect = function($item, $model, $label) {
c.sendAnalytics(c.showSuggestions ? "Suggestions" : "Typeahead");
c.searchTerm = ""; // prevents unexpected result if user quickly clicks search button after selecting
if (c.showSuggestions)
$item.url = "?id=search&q="+encodeURIComponent($item.name);

if(!$item.url || $item.url === "")
return;

if (!c.showSuggestions) {
var index = $(".typeahead-popup li.active").data('index');
c.trackSearchResultClicked(index + 1);
}

c.trackSearchClickedEvent($model);

if ($item.target)
window.open($item.url, $item.target);
else {
var newUrl = $location.url($item.url);
spAriaFocusManager.navigateToLink(newUrl.url());
}
};

function recordSuggestionsCount(){
c.instanceHistorySuggestionsCount = 0;
c.userHistorySuggestionsCount = 0;

c.searchItems.forEach(function(item){
return item.type === 'INSTANCE_HISTORY' ? c.instanceHistorySuggestionsCount++ : c.userHistorySuggestionsCount++;
});
}

function getSearchItemIndex(items, targetItem) {
return (items || []).findIndex(function(item) {
return item.name === targetItem.name;
});
}

function getRelativeSearchItemIndex(items, targetItem) {
var groupedItems = (items || []).filter(function(item) {
return item.type === targetItem.type;
});

return getSearchItemIndex(groupedItems, targetItem);
}

c.getSearchSuggestions = function(query) {
c.searchQuery = query;
if ($location.search().q == c.searchQuery)
return;

var payload = {
params: {
"sysparm_term" : c.searchQuery,
"sysparm_sp_portal_id": c.data.portalID,
"sysparm_suggestions_limit": c.suggestionsLimit,
"sysparm_search_sources": c.data.searchSourceSysIds || ""
},
headers : {'Accept' : 'application/json'}
};

var requestTimeStamp = new Date().getTime();
return $http.get("/api/now/search/sp_suggestions", payload).then(function(response){
var responseTimeStamp = new Date().getTime();
var responseTimeInMilliSeconds = (responseTimeStamp - requestTimeStamp);
var result = response.data.result;

c.totalSuggestionsCount = result != null ? result.entries.length : 0;
sendLiveMessage(c.totalSuggestionsCount);

c.searchItems = result.entries.map(function(item) {
item.query = getQueryToHighlight(item, c.searchQuery);
item.glyph = getIcon(item.type);
item.term = item.name;
return item;
});

recordSuggestionsCount();
c.trackSuggestionsRenderedEvent(query.length, responseTimeInMilliSeconds);
return c.searchItems;
});
   };

function getSearchSources(results, c) {
var sources = {};
c.data.searchSources.map(function(key) {
sources[key] = 0;
});
results.map(function(item) {
if(sources[item.type])
sources[item.type]++;
else
sources[item.type] = 1;
});
var searchSources = [];
Object.keys(sources).map(function(key) {
var source_id = c.data.searchSourceConfiguration[key] ? c.data.searchSourceConfiguration[key].sys_id : key;
searchSources.push({
source_id: source_id,
number_of_results: sources[key]
});
});
return searchSources;
}

function getSearchResultsSignals(results, c) {
return results.map(function(item) {
var recordId = item.sys_id;
var tableName = item.table;

return {
record_id: recordId,
table_name: tableName
};
});
}

function setUserLocationCoords(cb) {
var onSuccess = function(pos) {
return cb({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude
})
};

var onError = function() {
return cb({
latitude: null,
longitude: null
});
};

return window.navigator.geolocation.getCurrentPosition(onSuccess, onError, {
enableHighAccuracy: true
});
}

if(!c.isLocationTrackerDisabled) {
setUserLocationCoords(function(coords) {
c.latitude = coords.latitude;
c.longitude = coords.longitude;
});
}

function getResultDescription(result){
var description = null;
if(result.name != null)
description = result.name;
else if(result.primary != null)
description = result.primary;
else if(result.sec_title != null)
description = result.sec_title;
return description;
}

c.trackSearchResultClicked = function(rank) {
if (!rank || rank < 1) return ;
var query = c.latestQuery;
var results = _.get(c.data, 'results', []);
var result = results[rank-1];
var sourceTable = result.table != null ? result.table : null;

var payloadObject = {
action: "GlideSPSearchAnalyticsUpdateRank",
payload: {
query: query,
portal_id: this.data.portalID,
page_id: this.pageID,
results_per_source: getSearchSources(results, this),
search_results: getSearchResultsSignals(results, this),
refinement_occurred: false,
browser_info: $window.navigator.userAgent,
location: {
latitude: c.latitude,
longitude: c.longitude
},
result_event_sys_id : result.sys_id,
label_description : getResultDescription(result),
source_table: sourceTable,
signal_type: "CLICK",
signal_value: rank
}
};

$window.spSearchAnalytics = {
query: query
};
c.server.get(payloadObject);
}

c.getResults = function(query) {
c.searchQuery = query;
var payload = {
"query": c.searchQuery,
"portal": c.data.portalID,
"page": c.pageID,
"source": c.data.searchSources,
"include_facets": false,
"searchType": "typeahead"
};


if (c.options.limit || c.options.limit == 0)
payload.count = c.options.limit;

return $http.post("/api/now/sp/search", payload).then(function(response) {
// Prevents typeahead from displaying suggestions if queries from page and input are the same
if ($location.search().q == c.searchQuery)
return;

var result = response.data.result;
var resultCount = result != null ? result.results.length : 0
sendLiveMessage(resultCount);

c.data.results = result.results;
c.latestQuery = c.searchQuery;

c.searchItems = result.results.map(function(item) {
var config = c.data.searchSourceConfiguration[item.__search_source_id__];

if (!item.url && config.linkToPage) {
item.url = "?id=" + config.linkToPage;
if (item.sys_id)
item.url += "&sys_id=" + item.sys_id;
if (item.table)
item.url += "&table=" + item.table
}

if (config.type == "ADVANCED") {
item.templateID = config.template;
} else {
item.glyph = config.glyph;
}
return item;
});
if (c.searchItems.length == 0)
c.searchItems = [{"primary": c.data.noResultsFoundMsg}];

return c.searchItems;
});
}

c.searchType = c.data.searchType;
$scope.$on('$locationChangeSuccess', onLocationChangeSuccess);

function onLocationChangeSuccess(event, newUrl, oldUrl) {
if(searchSourceChanged(newUrl, oldUrl)) {
var newUrlParams = newUrl.match(/t=.+/);
if(!newUrlParams) {
c.searchType = null;
} else {
c.searchType = newUrlParams[0].split("&")[0].substring(2);
}
}
}

function searchSourceChanged(newUrl, oldUrl) {
var newUrlParams = newUrl.match(/t=.+/),
oldUrlParams = oldUrl.match(/t=.+/);

if(!newUrlParams && !oldUrlParams) {
return false;
}

if((!newUrlParams && oldUrlParams) || (newUrlParams && !oldUrlParams)) {
return true;
}

return newUrlParams[0].split("&")[0] !== oldUrlParams[0].split("&")[0];
}

c.submitSearch = function() {
c.sendAnalytics("User Entered");
var shouldReloadPage = c.data.refreshPageOnSearch && $location.search().id === 'search';

if (c.searchTerm) {
var newUrl = $location.search({
id: 'search',
spa: '1',
t: c.searchType,
q: c.searchTerm
});

if (shouldReloadPage)
$scope.$emit("sp.page.reload");

spAriaFocusManager.navigateToLink(newUrl.url());
//Pass the current page ID to search page for Search Analytics
$window.spSearchAnalytics = {
page_id: c.pageID
};
}
}

function sendLiveMessage(resultCount) {
spAriaUtil.sendLiveMessage(resultCount + " " +
c.data.resultMsg + " " +
(resultCount > 0 ? ' ' + c.data.navigationMsg : ''));
}

function getIcon(itemType) {
return itemType === "INSTANCE_HISTORY" ? 'search' : 'clock-o';
}

function getQueryToHighlight(item, query) {
return item.type === "INSTANCE_HISTORY" ? item.name.substring(query.length) : query;
}
}

 

 

 

1 ACCEPTED SOLUTION

Carlos Candano
Mega Guru

I made changes with OOTB. What I did is update the  typeahead template of Catalogs Search Source of /sp Service Portal

find_real_file.png

 

Code:

<!-- prefer item picture to item icon, prefer item icon to default icon -->
<div onclick="window.GlideWebAnalytics.trackEvent('Service Catalog', 'Catalog Search Type Ahead', 'Item Clicked')">
<i ng-if="match.model.picture" class="ta-img" style="background-image:url('{{match.model.picture}}?t=small')"></i>
<i ng-if="!match.model.picture && match.model.icon" class="ta-icon" style="background-image:url('{{match.model.icon}}'); width:16px; height:16px"></i>
<i ng-if="!match.model.picture && !match.model.icon" class="ta-icon fa fa-{{match.model.default_icon}}"></i>
<span ng-bind-html="match.label | uibTypeaheadHighlight:query"></span>
<p>
<span ng-bind-html="match.model.short_description | uibTypeaheadHighlight:query"></span>
  </p>
<strong ng-if="match.model.type == 'sc_content' && match.model.content_type == 'external'">âžš</strong>
</div>

Result:

find_real_file.png

View solution in original post

13 REPLIES 13

Sorry David, but I can't follow you. If user search for some text like "User" then Typeahead Search widget will display items from all Search Sources. Not all found items could have short_description property or not all Search Sources could include the data from the field. Thus I guess that you should make no modification of Typeahead Search widget and make modifications in the corresponding Search Source.

I posted the answer on very close question. If you need to display short description of catalog items, then it's enough to make minimal modification in Typeahead template to display short description of catalog items. Is it probably what you search for?

Anil Lande
Kilo Patron

Hi,

Can you please try modifying your script little bit?

You have something like this in your script to return ITEM name:

c.searchItems = result.entries.map(function(item) {
item.query = getQueryToHighlight(item, c.searchQuery);
item.glyph = getIcon(item.type);
item.term = item.name;
return item;
});

Please modify the line no 4 as given below:

c.searchItems = result.entries.map(function(item) {
item.query = getQueryToHighlight(item, c.searchQuery);
item.glyph = getIcon(item.type);
item.term = item.name+' '+item.short_description+'' ;  //Please change this line
return item;
});

Note: This is not good practice, but you can give a try.

 

Thanks,

Anil Lande

Please appreciate the efforts of community contributors by marking appropriate response as correct answer and helpful, this may help other community users to follow correct solution in future.
Thanks
Anil Lande

Thank You replying me, I'll check this

Hello Anil, This did not work 

Carlos Candano
Mega Guru

I made changes with OOTB. What I did is update the  typeahead template of Catalogs Search Source of /sp Service Portal

find_real_file.png

 

Code:

<!-- prefer item picture to item icon, prefer item icon to default icon -->
<div onclick="window.GlideWebAnalytics.trackEvent('Service Catalog', 'Catalog Search Type Ahead', 'Item Clicked')">
<i ng-if="match.model.picture" class="ta-img" style="background-image:url('{{match.model.picture}}?t=small')"></i>
<i ng-if="!match.model.picture && match.model.icon" class="ta-icon" style="background-image:url('{{match.model.icon}}'); width:16px; height:16px"></i>
<i ng-if="!match.model.picture && !match.model.icon" class="ta-icon fa fa-{{match.model.default_icon}}"></i>
<span ng-bind-html="match.label | uibTypeaheadHighlight:query"></span>
<p>
<span ng-bind-html="match.model.short_description | uibTypeaheadHighlight:query"></span>
  </p>
<strong ng-if="match.model.type == 'sc_content' && match.model.content_type == 'external'">âžš</strong>
</div>

Result:

find_real_file.png