Native UI KB article feedback comments
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Hi @all,
I have a requirement where I need t make mandatory comments for the KB article in Native UI so that users can submit the feedback for the KB article in Native UI when helpful "No" selected (popup needed with reasons for feedback) as mentioned in the screenshots and a feedback task to be created.
Anyone worked similar kind requirement please provide suggestions how to achieve this requirement.
Thanks in advance.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Hi @dev_S ,
1. Navigate to sys_ui_macro.list and open the record where Name is kb_view_ratings.
2. Update the XML field of this record with the following content.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g2:evaluate var="jvar_not_important" expression="
var show_kb_menu = gs.getProperty('glide.knowman.show_kb_menu', 'true');
var roles = gs.getProperty('glide.knowman.show_kb_menu.roles');
if (roles == null || roles == '')
;
else {
var hasRole = gs.hasRole(roles);
if (hasRole == false)
show_kb_menu = false;
}
"/>
<j2:if test="$[show_kb_menu]">
<j2:if test="$[sysparm_media != 'print']">
<g2:evaluate var="jvar_not_important" expression="
var show_kb_rating_options = gs.getProperty('glide.knowman.show_rating_options','true');
var roles = gs.getProperty('glide.knowman.show_rating_options.roles');
if (roles == null || roles == '')
;
else {
var hasRole = gs.hasRole(roles);
if (hasRole == false)
show_kb_rating_options = false;
}
"/>
<g2:evaluate var="jvar_not_important" expression="
var show_kb_yn_rating = gs.getProperty('glide.knowman.show_yn_rating','true');
if(knowledgeRecord.kb_knowledge_base.disable_mark_as_helpful)
show_kb_yn_rating = false;
var roles = gs.getProperty('glide.knowman.show_yn_rating.roles');
if (roles == null || roles == '')
;
else {
var hasRole = gs.hasRole(roles);
if (hasRole == false)
show_kb_yn_rating = false;
}
"/>
<g2:evaluate var="jvar_not_important" expression="
var show_user_feedback = kbViewInfo.hideFeedbackOptions ? false : gs.getProperty('glide.knowman.show_user_feedback') == 'onclick';
var roles = gs.getProperty('glide.knowman.show_user_feedback.roles');
if (roles == null || roles == '')
;
else {
var hasRole = gs.hasRole(roles);
if (hasRole == false)
show_user_feedback = false;
}
" />
<!-- NEW: same property the portal "Knowledge Article Helpful" widget reads; reasons come from the same
source as the portal popup (KBFeedbackTask.getReasonValues -> kb_feedback.reason choices). -->
<g2:evaluate var="jvar_not_important" expression="
var kb_actionable_helpful = gs.getProperty('glide.knowman.feedback.enable_actionable_feedback_for_helpful', 'false') == 'true';
var kb_reasons_json = kb_actionable_helpful ? (new KBFeedbackTask().getReasonValues() + '') : '[]';
" />
<g:evaluate var="jvar_failure_message" expression="gs.getMessage('Useful rating not submitted')" />
<j2:if test="$[show_kb_rating_options]">
<script>
function KbArticleRatingClass() {
var currentId = '$[JS:knowledgeRecord.sys_id]';
var feedbackRequestState = null;
var helpfulYesCount = parseInt("${kbViewInfo.helpfulYesCount || 0}");
var helpfulTotalCount = parseInt("${kbViewInfo.helpfulTotalCount || 0}");
var isSubmitting = false;
// ---- NEW: popup configuration (portal parity) ----
var actionableHelpful = ('$[JS:kb_actionable_helpful]' === 'true');
var feedbackReasons = [];
try { feedbackReasons = JSON.parse('$[JS:kb_reasons_json]' || '[]'); } catch (e) { feedbackReasons = []; }
var OTHER_REASON = '4';
// Portal does NOT enforce Details for "Other" in the UI (only server side, via
// glide.knowman.feedback.enable_explicit_feedback). Flip to true to also enforce it here.
var requireDetailsForOther = false;
var modalEl = null;
var lastFocus = null;
function ajaxPost(value, extra) {
if (isSubmitting) {
return;
}
isSubmitting = true;
if (currentId) {
var ajax = new GlideAjax('KBAjax');
// NEW: popup path uses saveUsefulWithFeedback, everything else stays on OOTB saveUseful
ajax.addParam('sysparm_type', extra ? 'saveUsefulWithFeedback' : 'saveUseful');
ajax.addParam('sysparm_useful', value);
ajax.addParam("sysparm_article_id", currentId);
if (extra) {
ajax.addParam('sysparm_reason', extra.reason);
ajax.addParam('sysparm_comments', extra.comments);
}
var query = $j('#sysparm_search');
ajax.addParam('sysparm_search', (query.length) ? escape(query.val()) : '');
ajax.addParam("ts_queryId", "${tsQueryId}");
ajax.getXMLAnswer(function(answer) {
postUsefulCallback(answer);
isSubmitting = false;
});
} else {
isSubmitting = false;
}
}
function postUsefulCallback(answer) {
var data = answer.evalJSON(true);
if (!data) {
return kbViewArticle.showErrorMessage('${jvar_failure_message}');
}
if (!data.success) {
return kbViewArticle.showErrorMessage(data.message);
}
helpfulTotalCount += 1;
if (feedbackRequestState === 'yes') {
helpfulYesCount += 1;
}
var newPercent = (helpfulTotalCount > 0) ? Math.round(helpfulYesCount / helpfulTotalCount * 100) : 0;
var messageTemplate = "${gs.getMessage('{0}% found this useful')}";
var notification = $j('#kb_feedback_notification');
if (notification.length > 0) {
notification.css({
'position': 'fixed',
'top': '10px',
'left': '10px',
'right': '10px',
'z-index': '9999',
'width': 'calc(100% - 20px)',
'max-width': 'none',
'margin': '0 auto',
'box-shadow': '0 2px 8px rgba(0,0,0,0.15)'
}).fadeIn(300, function() {
setTimeout(function() {
notification.find('.icon-cross.close').focus();
}, 100);
});
}
var percentageElement = $j('.kb-article-yn-percentage');
if (percentageElement.length > 0) {
if (newPercent > 0) {
messageTemplate = messageTemplate.replace("{0}", newPercent);
percentageElement.html(
'<span class="thankyou-text">' + "${gs.getMessage('Thank you for your feedback!')}" + '</span> ' +
'<span class="thankyou-percent" style="padding-left: 20px;">' + messageTemplate + '</span>'
).css({
'display': 'block',
'opacity': 0
}).animate({
opacity: 1
}, 300);
} else {
percentageElement.html(
'<span class="thankyou-text">' + "${gs.getMessage('Thank you for your feedback!')}" + '</span>'
).css({
'display': 'block',
'opacity': 0
}).animate({
opacity: 1
}, 300);
}
}
}
// NEW: common submit step (fade out the buttons, then post)
function submitFeedback(value, extra) {
feedbackRequestState = value;
var box = $j('.kb_box_centered');
box.find('.kb-article-yn-label, .kb-article-yes-btn, .kb-article-no-btn, .kb-article-yn-percentage').fadeOut(200, function() {
ajaxPost(value, extra);
});
}
// ---- NEW: popup (mirrors portal kb-feedback-task-modal) ----
function el(tag, cls, text) {
var node = document.createElement(tag);
if (cls) { node.className = cls; }
if (text) { node.textContent = text; }
return node;
}
function ensureModalStyles() {
if (document.getElementById('kbfb_styles')) { return; }
var css = [
'.kbfb-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:center;justify-content:center;}',
'.kbfb-dialog{background:#fff;color:#2b2b2b;width:520px;max-width:92%;max-height:90%;overflow:auto;border-radius:4px;box-shadow:0 6px 24px rgba(0,0,0,.35);}',
'.kbfb-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #ddd;}',
'.kbfb-title{margin:0;font-size:18px;font-weight:600;}',
'.kbfb-close{background:transparent;border:0;font-size:22px;line-height:1;cursor:pointer;color:inherit;padding:4px 8px;}',
'.kbfb-body{padding:16px 20px 20px;}',
'.kbfb-desc{margin:0 0 12px;}',
'.kbfb-fieldset{border:0;margin:0 0 12px;padding:0;}',
'.kbfb-legend{font-weight:600;margin-bottom:6px;font-size:14px;}',
'.kbfb-radio{display:block;margin:0 0 6px;cursor:pointer;}',
'.kbfb-radio input{margin-right:8px;}',
'.kbfb-label{display:block;font-weight:600;margin-bottom:4px;}',
'.kbfb-text{width:100%;box-sizing:border-box;resize:vertical;min-height:70px;padding:6px 8px;}',
'.kbfb-error{color:#b00020;min-height:18px;margin-top:6px;}',
'.kbfb-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}'
].join('');
var st = document.createElement('style');
st.id = 'kbfb_styles';
st.appendChild(document.createTextNode(css));
document.head.appendChild(st);
}
function openFeedbackModal() {
if (modalEl) { return; }
ensureModalStyles();
lastFocus = document.activeElement;
var overlay = el('div', 'kbfb-overlay');
var dlg = el('div', 'kbfb-dialog');
dlg.setAttribute('role', 'dialog');
dlg.setAttribute('aria-modal', 'true');
dlg.setAttribute('aria-labelledby', 'kbfb_title');
dlg.setAttribute('aria-describedby', 'kbfb_desc');
var header = el('div', 'kbfb-header');
var title = el('h2', 'kbfb-title', "${gs.getMessage('Thank you for the feedback')}");
title.id = 'kbfb_title';
var closeBtn = el('button', 'kbfb-close', '\u00D7');
closeBtn.type = 'button';
closeBtn.setAttribute('aria-label', "${gs.getMessage('Close')}");
header.appendChild(title);
header.appendChild(closeBtn);
var body = el('div', 'kbfb-body');
var desc = el('p', 'kbfb-desc', "${gs.getMessage('Please provide additional details')}");
desc.id = 'kbfb_desc';
body.appendChild(desc);
var fs = el('fieldset', 'kbfb-fieldset');
fs.appendChild(el('legend', 'kbfb-legend', "${gs.getMessage('Reason')}"));
feedbackReasons.forEach(function(r, idx) {
var lbl = el('label', 'kbfb-radio');
var inp = document.createElement('input');
inp.type = 'radio';
inp.name = 'kbfb_reason';
inp.id = 'kbfb_reason_' + idx;
inp.value = r.reason_id;
inp.checked = (String(r.reason_id) === OTHER_REASON); // portal default = Other
lbl.appendChild(inp);
lbl.appendChild(document.createTextNode(r.reason_desc));
fs.appendChild(lbl);
});
body.appendChild(fs);
var detailsLbl = el('label', 'kbfb-label', "${gs.getMessage('Details')}");
detailsLbl.setAttribute('for', 'kbfb_details');
var details = el('textarea', 'kbfb-text');
details.id = 'kbfb_details';
details.rows = 3;
body.appendChild(detailsLbl);
body.appendChild(details);
var err = el('div', 'kbfb-error');
err.setAttribute('role', 'alert');
err.setAttribute('aria-live', 'assertive');
body.appendChild(err);
var actions = el('div', 'kbfb-actions');
var noThanks = el('button', 'btn btn-default', "${gs.getMessage('No thanks')}");
noThanks.type = 'button';
var submit = el('button', 'btn btn-primary', "${gs.getMessage('Submit')}");
submit.type = 'button';
actions.appendChild(noThanks);
actions.appendChild(submit);
body.appendChild(actions);
dlg.appendChild(header);
dlg.appendChild(body);
overlay.appendChild(dlg);
document.body.appendChild(overlay);
modalEl = overlay;
function selectedReason() {
var checked = dlg.querySelector('input[name="kbfb_reason"]:checked');
return checked ? checked.value : OTHER_REASON;
}
closeBtn.onclick = function() { closeFeedbackModal(false); };
noThanks.onclick = function() { closeFeedbackModal(false); };
overlay.onclick = function(ev) { if (ev.target === overlay) { closeFeedbackModal(false); } };
submit.onclick = function() {
var reason = selectedReason();
var text = details.value.trim();
if (requireDetailsForOther) {
if (reason === OTHER_REASON) {
if (!text) {
err.textContent = "${gs.getMessage('Please provide the mandatory details')}";
details.focus();
return;
}
}
}
closeFeedbackModal(true, reason, text);
};
dlg.onkeydown = function(ev) {
if (ev.key === 'Escape') {
ev.preventDefault();
closeFeedbackModal(false);
return;
}
if (ev.key === 'Tab') {
var f = dlg.querySelectorAll('button, input, textarea');
var first = f[0];
var last = f[f.length - 1];
if (ev.shiftKey) {
if (document.activeElement === first) { ev.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { ev.preventDefault(); first.focus(); }
}
}
};
var checkedRadio = dlg.querySelector('input[name="kbfb_reason"]:checked');
(checkedRadio || closeBtn).focus();
}
// Portal parity: the vote is ALWAYS saved when the popup closes.
// Submit -> chosen reason + details
// No thanks / X / Esc / overlay -> reason Other, empty details
function closeFeedbackModal(submitted, reason, text) {
if (!modalEl) { return; }
if (modalEl.parentNode) { modalEl.parentNode.removeChild(modalEl); }
modalEl = null;
if (lastFocus) { try { lastFocus.focus(); } catch (e) {} }
submitFeedback('no', {
reason: submitted ? reason : OTHER_REASON,
comments: submitted ? text : ''
});
}
function feedback(value) {
if (feedbackRequestState === value) {
return;
}
if (isSubmitting) {
return;
}
if (modalEl) {
return;
}
// NEW: No + property on + reasons available -> popup first, save when it closes
if (value === 'no') {
if (actionableHelpful) {
if (feedbackReasons.length > 0) {
openFeedbackModal();
return;
}
}
}
submitFeedback(value, null);
}
function close() {
var notification = $j('#kb_feedback_notification');
notification.fadeOut(300, function() {
notification.css('display', 'none');
});
}
return {
feedback: feedback,
close: close
};
}
// Create instance
if (typeof kbArticleRating === 'undefined') {
var kbArticleRating = new KbArticleRatingClass();
}
</script>
<j2:if test="$[show_kb_yn_rating]">
<j2:if test="$[sysparm_no_rating != true]">
<div class="kb_box_centered">
<span class="sr-only">${gs.getMessage('Was this article helpful?') + ' ' + kbViewInfo.helpfulText}</span>
<span class="kb-article-yn-label" >
<span id="kb_helpful_yn_label" aria-hidden="true">${gs.getMessage('Helpful?')}</span>
</span>
<span role="group" aria-label="${gs.getMessage('Was this article helpful?')}" class="kb-article-yn-box">
<button class="btn btn-success-subdued kb-article-yes-btn"
onclick="kbArticleRating.feedback('yes')" aria-label="${gs.getMessage('Yes,this article was helpful')}">${gs.getMessage('Yes')}</button>
<button class="btn btn-destructive-subdued kb-article-no-btn"
onclick="kbArticleRating.feedback('no')" aria-label="${gs.getMessage('No,this article was not helpful')}">${gs.getMessage('No')}</button>
<span class="kb-article-yn-percentage" aria-hidden="true">${kbViewInfo.helpfulText}</span>
</span>
</div>
<div id="kb_feedback_notification" class="notification notification-closable notification-info notification_message_sticky" style="display:none">
<span class="icon-cross close" tabindex="0" role="button" aria-label="${gs.getMessage('Close Notification')}" onclick="kbArticleRating.close()"></span>
<span role="alert">${gs.getMessage('Feedback submitted.')}</span>
</div>
</j2:if>
</j2:if>
</j2:if>
</j2:if>
</j2:if>
</j:jelly>3. Navigate to sys_script_include.LIST and open the record where Name is KBAjax.
4.Update the script with following script.
/**
* Script Include : KBAjax (global, sys_id be0c299eeb7121007128a5fc5206fe9b)
* What it does
* - Same server path the portal uses: builds the params and calls this.saveUsefulWithParams(), which
* copies every key onto a kb_feedback insert (verified in KBAjaxSNC).
* - Because reason is now populated, the existing BR "Knowledge Feedback Task Creation"
* (kb_feedback, after insert, filter reason!=empty, condition canCreateFeedbackTask(current))
* does the task creation, including the enable_explicit_feedback "Other needs comments" rule.
*/
var KBAjax = Class.create();
KBAjax.prototype = Object.extendsObject(KBAjaxSNC, {
MAX_FEEDBACK_COMMENT_LENGTH: 4000, // adjust to the kb_feedback.comments max length on your instance
/**
* Called from the kb_view_ratings macro when the user finishes the "Thank you for the feedback" popup.
* Params : sysparm_article_id, sysparm_useful ('no' only), sysparm_reason, sysparm_comments,
* sysparm_query (optional), ts_queryId (optional)
* Returns: JSON string from saveUsefulWithParams ({success, message, recordId})
*/
saveUsefulWithFeedback: function() {
var fields = {};
fields.article = this.getParameter("sysparm_article_id") || "";
// Only the "No" vote carries a reason. Anything else fails the nil check in saveUsefulWithParams.
fields.useful = (this.getParameter("sysparm_useful") + "" == "no") ? "no" : "";
fields.user = gs.getUserID();
fields.query = this.getParameter("sysparm_query") || "";
fields.search_id = this.getParameter("ts_queryId") || "";
fields.session_id = gs.getSessionID();
var reason = (this.getParameter("sysparm_reason") || "") + "";
if (this._isValidFeedbackReason(reason)) {
fields.reason = reason;
var comments = ((this.getParameter("sysparm_comments") || "") + "").trim();
if (comments)
fields.comments = comments.substring(0, this.MAX_FEEDBACK_COMMENT_LENGTH);
}
return this.saveUsefulWithParams(fields);
},
// Reasons are validated against the kb_feedback.reason choice list (same source the portal popup uses)
_isValidFeedbackReason: function(reason) {
if (!reason)
return false;
var reasons = new global.JSON().decode(new KBFeedbackTask().getReasonValues());
for (var i = 0; i < reasons.length; i++) {
if (String(reasons[i].reason_id) === String(reason))
return true;
}
return false;
},
type: "KBAjax"
});5. Open any Knowledge Article in the Native UI and select No under Helpful. Verify that the pop-up appears as shown below.
6.Fill the details in the pop up and click on submit.
7. Navigate to kb_feedback_task.list and verify that a task record is created with the specified Reason and the details in the Description field.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
Hi @Rakesh_M ,
Thank you so much for your help it really helped me.
I made Details mandatory for all reasons as per my requirement as mentioned in the screenshot but the only thing is if I click on " x " popup close or click on " no thanks" button or click on outside the popup getting the message as info message "Feedback submitted." on top and "Thank you for your feedback!" at the bottom though no feedback record is created in feedback table.
It will be better if it just cancels the popup as buttons are available as before.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
submit.onclick = function() {
