Implementing .xlsx Data Export Functionality in Custom ServiceNow Widgets

vmanojkumar
Tera Contributor

I have a requirement to implement data export functionality in .xlsx format for custom widgets built in ServiceNow.

The expectation is to export the widget data into an Excel file while preserving details such as selected columns, column order, and data formatting wherever applicable.

I would appreciate it if anyone could share:

  • Relevant documentation links
  • Best practices or recommended approaches
  • Sample implementations/scripts
  • Supporting articles or community posts related to .xlsx export in custom widgets/service portals/workspaces

Any guidance on available APIs, utilities, or reusable solutions in ServiceNow would be really helpful.

Thanks in advance!

1 ACCEPTED SOLUTION

Vishal Jaswal
Tera Sage

Hello @vmanojkumar 

I would recommend to try below learning in your PDI. Open Service portal home and navigate to Requests and ensure you have few created/listed here.

The URL will be https://yourPDI.service-now.com/sp?id=my_requests

In the middle of this page, you can press ctrl (windows) or control (mac) and left click to see Widget in Editor which you can click (you can release the ctrl/control key) and clone the widget or navigate to Service Portal > Widgets and search for "My Requests". Clone it as "My Requests1" and check the test page option.

Now, the new URL will be The URL will be https://yourPDI.service-now.com/sp?id=my_requests1

You can open this "My Requests1" widget in widget editor (under related links of any widget form) and update the HTML Template as:

NOTE: Look for Comment Modifed Starts here and ends here to know what has been updated in the out-of-the-box widget to achieve this requirement.

<div class="panel panel-default b" ng-init="c.trackPage()">
 <div class="panel-heading" ng-show="::!data.is_associated_ticket_tab">

    <h2 class="panel-title">{{::data.messages.myRequestsTitle}}</h2>
  </div>
  
         <!-- MODIFIED Download button Starts here -->
   
      <button class="btn btn-primary" ng-click="c.downloadMyRequestsXlsx()">
        Download XLSX
      </button>
  <div ng-include="'myRequestsTemplate'" ng-show="::c.data.hide_draft_tab"></div>

 <!-- MODIFIED Download button Ends here -->
   


  <uib-tabset ng-hide="::c.data.hide_draft_tab">
    <uib-tab ng-repeat="tab in c.tabs" class="uib-vis-tab" heading="{{tab.heading}}" active="tab.active" index="{{tab.id}}" ng-select="{{c.changeSelectedTab(tab.id)}}" add-uib-tab-accessibility>
        <span ng-include="tab.template"></span>
    </uib-tab>
  </uib-tabset>
  




<script type="text/ng-template" id="draftItemsTemplate">
  <div class="panels-container list-group">





<div ng-show="::c.data.draftItems.length > 0" class="list-group-item row requests-header-container">
          <div class="col-md-4 col-xs-12 padding-left-large fit-content">
            <div class="input-group" style="width:100%">
              <input ng-model="c.data.draftSearchText" ng-change="c.updateFilteredDraftItems()" class="form-control" style="width:100%" placeholder="{{c.data.draftFilterMsg}}" aria-label="{{c.data.draftFilterMsg}}">
              <span class="input-group-btn">
                <button class="btn btn-default align-icon" type="button" data-original-title="{{c.data.draftFilterMsg }}" aria-label="{{c.data.draftFilterMsg }}" data-toggle="tooltip" data-placement="bottom">
                	<i class="fa fa-search"></i>
                </button>
              </span>
            </div><!-- /input-group -->
          </div>
    	</div>
      <div ng-if="c.data.draftItems.length > 0 && (!c.filteredDraftItems || c.filteredDraftItems.length == 0)" class="panel-body panels-container">
        ${Search didn't match any draft items}
      </div>

   <div ng-if="c.data.draftItems.length == 0" class="panel-body panels-container panel-align">
        ${You do not have any draft items}
   </div>
      <div role="table" ng-if="(c.filteredDraftItems && c.filteredDraftItems.length > 0)" class="table" aria-label="{{::c.data.draftItemsMsg}}">
      <div role="rowgroup" class="column-headers">
        <div role="row" class="list-group-item table-responsive">
          <span role="columnheader" class="col-xs-4 padder-r-none padder-l-none">${Draft name}</span>
          <span role="columnheader" class="col-xs-4 padder-md">${Item name}</span>
          <span role="columnheader" class="col-xs-2 padder-r-none padder-l-none">${my_requests_updated_capital}</span>
           <span role="columnheader" class="col-xs-1 padder-r-none padder-l-none"></span>
        </div>
   	  </div>
      <div role="rowgroup" class="padder-l-none padder-r-none">
        <div role="row" ng-repeat="item in c.filteredDraftItems" class="list-group-item draft-item-row table-responsive" style="margin:0px" >
          <div role="cell" class="draft-item-cell col-xs-4 padder-l-none padder-r-none main-column">
            <div class="primary-display text-wrap">
              <a href="?id={{'sc_cat_item'}}&{{'edit=draft'}}&sys_id={{::item.sys_id}}" aria-label="{{::item.cart_item_name}}"> {{::item.cart_item_name}} </a>
            </div>
          </div>
          <div role="cell" class="draft-item-cell col-xs-4 padder-md">
            <div class="text-wrap">
              <span> {{::item.name}}</span>
            </div>
          </div>
          <div role="cell" class="draft-item-cell col-xs-2 padder-l-none padder-r-none updated-column">
            <div class="updated">
              <i class="fa fa-clock-o" aria-hidden="true" title="${Updated}"></i>
              <sn-time-ago timestamp="::item.sys_updated_on"/>
            </div>
          </div>
          <span class="col-xs-1 padder-l-none padder-r-none" aria-hidden="true" id="draft-item-tooltip-{{::item.sys_id}}"></span>
          <div  role="cell" class="draft-item-cell col-xs-1 padder-l-none padder-r-none">
            <div class="btn-group btn-group-sm">
              <button type="button" class="btn btn-clear" ng-click="c.removeDraftItem(item)" aria-label="{{::c.translateDeleteDraftMsg(item.cart_item_name)}}" data-toggle="tooltip" data-placement="left"  data-container="#draft-item-tooltip-{{::item.sys_id}}" title="${Delete Draft Item}">
              	<i class=" icon-trash btn-remove" aria-hidden="true"></i>
              </button>
            </div>
           </div>
          </td>
        </div>

      </div>
       </div>
       </div>
       <div class="col-sm-12 pull-none" ng-if="c.data.hasMoreDrafts" style="padding-bottom:15px">
          <div class="text-a-c" ng-if="c.fetchingDrafts">
			  <i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
			  <span class="sr-only">${Loading more draft items}</span>
          </div>
          <button class="btn btn-default btn-show-more" ng-click="c.loadMoreDrafts()">${Show more drafts}</button>
      </div>
</script>

<script type="text/ng-template" id="myRequestsTemplate">
    <div class="panels-container list-group">
			<div ng-show="::!data.is_associated_ticket_tab" class="list-group-item row requests-header-container">
          <div class="col-md-3 col-xs-12 m-b-sm fit-content">
            <div class="form-inline control-view" ng-if="c.options.show_view == 'true'">
            	<label class="control-label hidden-xs wrapper-xs " id="label_view" for="view">${View}</label>
              <select ng-model="c.viewFilter" id="view" class="form-control adjust-width" ng-change="c.changeView()" style="width:80%"
                      ng-options="item.key as item.value for item in c.filterOptions">
              </select>
            </div>
          </div>
          <div class="col-md-4 col-xs-12 padding-left-large fit-content">
            <div class="input-group" style="width:100%">
              <input ng-model="c.filterText" ng-keypress="c.checkEnter($event)"class="form-control" style="width:100%" placeholder="{{data.filterMsg}}" aria-label="{{data.filterMsg}}">
              <span class="input-group-btn">
                <button class="btn btn-default align-icon" type="button" ng-click="c.search()" data-original-title="{{data.filterMsg}}" aria-label="{{data.filterMsg}}" data-toggle="tooltip" data-placement="bottom">
                	<i class="fa fa-search"></i>
                </button>
              </span>
            </div><!-- /input-group -->

          </div>
    	</div>

    	<div ng-if="c.data.request.req_list.length == 0 && !c.filterText" class="panel-body panels-container panel-align">
        ${You do not have any requests} 
      </div>
    	<div ng-if="c.data.request.req_list.length == 0 && c.filterText" class="panel-body panels-container">
        ${Search didn't match any requests} 
      </div>
      <div role="table" ng-if="c.data.request.req_list.length > 0" class="table" aria-label="{{::data.messages.myRequestsTitle}}">
      <div ng-show="::!data.is_associated_ticket_tab" role="rowgroup" class="column-headers">
        <div role="row" class="list-group-item table-responsive">
          <span role="columnheader" class="col-xs-6 padder-r-none padder-l-none">${Request}</span>
          <span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${State}</span>
          <span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${my_requests_updated_capital}</span>
        </div>
   	  </div>
      <div role="rowgroup" class="padder-l-none padder-r-none">
        <div role="row" class="list-group-item table-responsive" ng-repeat="item in c.data.request.req_list | limitTo: c.data.lastLimit track by item.sys_id" style="margin:0px" >
          <div role="cell" class="col-xs-6 padder-l-none padder-r-none main-column">
            <div class="primary-display">
              <a href="?id={{::item.url.id}}&table={{::item.url.table}}&sys_id={{::item.url.sys_id}}" sn-focus="{{::item.highlight}}" aria-label="{{::item.display_field}} , {{::item.display_number}}"> {{::item.display_field}} </a>
            </div>
            <small class="text-muted">
              <div ng-repeat="f in item.secondary_displays" class="secondary-display">
                <span aria-hidden="true">{{::f.display_value}}</span>
              </div>
            </small>
          </div>
          <div role="cell" class="col-xs-3 padder-l-none padder-r-none state-column">
            <div class="state">
              <span tabindex= "0"> {{::item.state}}</span>
            </div>
          </div>
          <div role="cell" class="col-xs-3 padder-l-none padder-r-none updated-column">
            <div class="updated">
              <i class="fa fa-clock-o" aria-hidden="true" title="${Updated}"></i>
              <sn-time-ago timestamp="::item.updated_on"/>
            </div>
          </div>
        </div>
      </div>
    </div>

      <div class="col-sm-12 pull-none" ng-if="c.data.hasMore" style="padding-bottom:15px">
					<div class="text-a-c" ng-if="c.fetching">
          	<i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
						<span class="sr-only">${Loading more requests}</span>  
  	      </div>
          <button class="btn btn-default btn-show-more" ng-click="c.loadMore()"> {{::data.messages.showMoreRequests}} </button>  
      </div>
    
</script>


Client Script as:

function ($scope, $window, spAriaUtil, i18n, spModal) {
  var c = this;
  // MODIFIED Code Starts here
  if (!$window.XLSX) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js";
    script.onload = function () {
      console.log("XLSX loaded dynamically for My Requests");
    };
    document.head.appendChild(script);
  }


  c.downloadMyRequestsXlsx = function () {
    // Export whatever is currently loaded on the page
    var rows = (c.data && c.data.request && c.data.request.req_list) ? c.data.request.req_list : [];

    if (!rows || !rows.length) {
      alert("No data available to export.");
      return;
    }

    if (typeof $window.XLSX === "undefined" || !$window.XLSX) {
      alert("XLSX library is not loaded on this portal page.");
      return;
    }

    // Map My Requests row-shape into export-friendly columns
    var exportData = rows.map(function (item) {
      return {
        "Request": item.display_field || item.display_number || "",
        "State": item.state || "",
        "Updated": item.updated_on || "",
        "Number": item.display_number || item.number || "",
        "Short Description": item.short_description || ""
      };
    });

    var ws = $window.XLSX.utils.json_to_sheet(exportData);
    var wb = $window.XLSX.utils.book_new();
    $window.XLSX.utils.book_append_sheet(wb, ws, "My Requests");

    var ts = new Date().toISOString().slice(0, 10);
    $window.XLSX.writeFile(wb, "My_Requests_" + ts + ".xlsx");
  };


  // MODIFIED Code Ends here

  c.translateDeleteDraftMsg = function (draftItemName) {
    return i18n.getMessage("Delete {0}").withValues([draftItemName]);
  };

  c.trackPage = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "My Requests Widget Loaded");
  };

  if (c.data.is_new_order)
    spAriaUtil.sendLiveMessage(c.data.requestSubmitMsg);

  c.viewFilter = "open";
  c.filterOptions = [
    { key: "open", value: c.data.messages.openRequests },
    { key: "close", value: c.data.messages.closedRequests }
  ];

  c.tabs = [
    { id: "requests", template: "myRequestsTemplate", heading: c.data.messages.requestsTabLabel, active: c.data.isRequestsTabActive },
    { id: "drafts", template: "draftItemsTemplate", heading: c.data.messages.draftsTabLabel, active: c.data.isDraftsTabActive }
  ];

  c.changeView = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "Open/Close Filter Toggled");
    c.server.get({
      action: "change_view",
      view: c.viewFilter,
      search_text: c.filterText
    }).then(function (response) {
      c.data = response.data;
      var resultMsg = (c.data.request.req_list.length === 1 ? "result" : "results") + " returned";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([c.data.request.req_list.length]));
    });
  };

  c.search = function () {
    c.server.get({
      action: "search",
      search_text: c.filterText,
      view: c.viewFilter
    }).then(function (response) {
      c.data = response.data;
      var resultMsg = (c.data.request.req_list.length === 1 ? "result" : "results") + " returned";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([c.data.request.req_list.length]));
    });
  };

  c.checkEnter = function (event) {
    if (event.which === 13)
      c.search();
  };

  c.filterDraftItem = function (item) {
    if (!c.data.draftSearchText)
      return true;

    var searchText = c.data.draftSearchText.toLowerCase();
    return (
      (item.name && item.name.toLowerCase().includes(searchText)) ||
      (item.cart_item_name && item.cart_item_name.toLowerCase().includes(searchText))
    );
  };

  c.updateFilteredDraftItems = function () {
    if (!c.data.draftItems)
      return;

    if (!c.data.draftSearchText)
      c.filteredDraftItems = c.data.draftItems;
    else {
      c.filteredDraftItems = c.data.draftItems.filter(function (item) {
        return c.filterDraftItem(item);
      });
    }
  };

  c.filteredDraftItems = [];
  c.updateFilteredDraftItems();

  c.removeDraftItem = function (cartItem) {
    var options = {
      title: c.data.deleteDraftItemMsg,
      headerStyle: { border: "none", "padding-bottom": 0 },
      footerStyle: { border: "none", "padding-top": 0 },
      messageOnly: true,
      buttons: [
        { label: c.data.dialogCancel, primary: false },
        { label: c.data.dialogDelete, "class": "btn-danger", primary: true }
      ]
    };

    spModal.open(options).then(function (actionButton) {
      if (actionButton) {
        if (!actionButton.primary)
          return;

        c.fetchingDrafts = true;
        c.server.get({
          action: "remove_item",
          removeItemID: cartItem.sys_id,
          prevLimit: (c.data.draftItems && c.data.draftItems.length) ? c.data.draftItems.length : 100,
          attachmentTable: cartItem.attachment_table
        }).then(function (response) {
          c.data.draftItems = response.data.draftItems;
          c.data.hasMoreDrafts = response.data.hasMoreDrafts;
          c.updateFilteredDraftItems();
          c.fetchingDrafts = false;
        });
      }
    });
  };

  c.loadMore = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "Show More Clicked");
    var currentRequests = c.data.request.req_list.length;
    spAriaUtil.sendLiveMessage("${Loading more requests}");
    c.fetching = true;

    c.server.get({
      action: "fetch_more",
      lastLimit: c.data.lastLimit,
      view: c.viewFilter,
      search_text: c.filterText
    }).then(function (response) {
      var addedRequests = response.data.request.req_list.length - currentRequests;
      var resultMsg = (addedRequests === 1 ? "request" : "requests") + " added";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([addedRequests]));
      c.data = response.data;
      c.fetching = false;
    });
  };

  c.loadMoreDrafts = function () {
    c.fetchingDrafts = true;

    c.server.get({
      action: "fetch_more_draft_items",
      prevLimit: (c.data.draftItems && c.data.draftItems.length) ? c.data.draftItems.length : 100
    }).then(function (response) {
      c.data.draftItems = response.data.draftItems;
      c.data.hasMoreDrafts = response.data.hasMoreDrafts;
      c.updateFilteredDraftItems();
      c.fetchingDrafts = false;
    });
  };

  c.changeSelectedTab = function (selectedTab) {
    c.data.isRequestsTabActive = (selectedTab === "requests");
    c.data.isDraftsTabActive = (selectedTab === "drafts");
  };
}

 

Before:

VishalJaswal_4-1778536383370.png

 


After:

VishalJaswal_3-1778536361942.png

 



VishalJaswal_1-1778536343156.png

VishalJaswal_0-1778536566169.png

 






Hope that helps!

View solution in original post

4 REPLIES 4

Vishal Jaswal
Tera Sage

Hello @vmanojkumar 

I would recommend to try below learning in your PDI. Open Service portal home and navigate to Requests and ensure you have few created/listed here.

The URL will be https://yourPDI.service-now.com/sp?id=my_requests

In the middle of this page, you can press ctrl (windows) or control (mac) and left click to see Widget in Editor which you can click (you can release the ctrl/control key) and clone the widget or navigate to Service Portal > Widgets and search for "My Requests". Clone it as "My Requests1" and check the test page option.

Now, the new URL will be The URL will be https://yourPDI.service-now.com/sp?id=my_requests1

You can open this "My Requests1" widget in widget editor (under related links of any widget form) and update the HTML Template as:

NOTE: Look for Comment Modifed Starts here and ends here to know what has been updated in the out-of-the-box widget to achieve this requirement.

<div class="panel panel-default b" ng-init="c.trackPage()">
 <div class="panel-heading" ng-show="::!data.is_associated_ticket_tab">

    <h2 class="panel-title">{{::data.messages.myRequestsTitle}}</h2>
  </div>
  
         <!-- MODIFIED Download button Starts here -->
   
      <button class="btn btn-primary" ng-click="c.downloadMyRequestsXlsx()">
        Download XLSX
      </button>
  <div ng-include="'myRequestsTemplate'" ng-show="::c.data.hide_draft_tab"></div>

 <!-- MODIFIED Download button Ends here -->
   


  <uib-tabset ng-hide="::c.data.hide_draft_tab">
    <uib-tab ng-repeat="tab in c.tabs" class="uib-vis-tab" heading="{{tab.heading}}" active="tab.active" index="{{tab.id}}" ng-select="{{c.changeSelectedTab(tab.id)}}" add-uib-tab-accessibility>
        <span ng-include="tab.template"></span>
    </uib-tab>
  </uib-tabset>
  




<script type="text/ng-template" id="draftItemsTemplate">
  <div class="panels-container list-group">





<div ng-show="::c.data.draftItems.length > 0" class="list-group-item row requests-header-container">
          <div class="col-md-4 col-xs-12 padding-left-large fit-content">
            <div class="input-group" style="width:100%">
              <input ng-model="c.data.draftSearchText" ng-change="c.updateFilteredDraftItems()" class="form-control" style="width:100%" placeholder="{{c.data.draftFilterMsg}}" aria-label="{{c.data.draftFilterMsg}}">
              <span class="input-group-btn">
                <button class="btn btn-default align-icon" type="button" data-original-title="{{c.data.draftFilterMsg }}" aria-label="{{c.data.draftFilterMsg }}" data-toggle="tooltip" data-placement="bottom">
                	<i class="fa fa-search"></i>
                </button>
              </span>
            </div><!-- /input-group -->
          </div>
    	</div>
      <div ng-if="c.data.draftItems.length > 0 && (!c.filteredDraftItems || c.filteredDraftItems.length == 0)" class="panel-body panels-container">
        ${Search didn't match any draft items}
      </div>

   <div ng-if="c.data.draftItems.length == 0" class="panel-body panels-container panel-align">
        ${You do not have any draft items}
   </div>
      <div role="table" ng-if="(c.filteredDraftItems && c.filteredDraftItems.length > 0)" class="table" aria-label="{{::c.data.draftItemsMsg}}">
      <div role="rowgroup" class="column-headers">
        <div role="row" class="list-group-item table-responsive">
          <span role="columnheader" class="col-xs-4 padder-r-none padder-l-none">${Draft name}</span>
          <span role="columnheader" class="col-xs-4 padder-md">${Item name}</span>
          <span role="columnheader" class="col-xs-2 padder-r-none padder-l-none">${my_requests_updated_capital}</span>
           <span role="columnheader" class="col-xs-1 padder-r-none padder-l-none"></span>
        </div>
   	  </div>
      <div role="rowgroup" class="padder-l-none padder-r-none">
        <div role="row" ng-repeat="item in c.filteredDraftItems" class="list-group-item draft-item-row table-responsive" style="margin:0px" >
          <div role="cell" class="draft-item-cell col-xs-4 padder-l-none padder-r-none main-column">
            <div class="primary-display text-wrap">
              <a href="?id={{'sc_cat_item'}}&{{'edit=draft'}}&sys_id={{::item.sys_id}}" aria-label="{{::item.cart_item_name}}"> {{::item.cart_item_name}} </a>
            </div>
          </div>
          <div role="cell" class="draft-item-cell col-xs-4 padder-md">
            <div class="text-wrap">
              <span> {{::item.name}}</span>
            </div>
          </div>
          <div role="cell" class="draft-item-cell col-xs-2 padder-l-none padder-r-none updated-column">
            <div class="updated">
              <i class="fa fa-clock-o" aria-hidden="true" title="${Updated}"></i>
              <sn-time-ago timestamp="::item.sys_updated_on"/>
            </div>
          </div>
          <span class="col-xs-1 padder-l-none padder-r-none" aria-hidden="true" id="draft-item-tooltip-{{::item.sys_id}}"></span>
          <div  role="cell" class="draft-item-cell col-xs-1 padder-l-none padder-r-none">
            <div class="btn-group btn-group-sm">
              <button type="button" class="btn btn-clear" ng-click="c.removeDraftItem(item)" aria-label="{{::c.translateDeleteDraftMsg(item.cart_item_name)}}" data-toggle="tooltip" data-placement="left"  data-container="#draft-item-tooltip-{{::item.sys_id}}" title="${Delete Draft Item}">
              	<i class=" icon-trash btn-remove" aria-hidden="true"></i>
              </button>
            </div>
           </div>
          </td>
        </div>

      </div>
       </div>
       </div>
       <div class="col-sm-12 pull-none" ng-if="c.data.hasMoreDrafts" style="padding-bottom:15px">
          <div class="text-a-c" ng-if="c.fetchingDrafts">
			  <i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
			  <span class="sr-only">${Loading more draft items}</span>
          </div>
          <button class="btn btn-default btn-show-more" ng-click="c.loadMoreDrafts()">${Show more drafts}</button>
      </div>
</script>

<script type="text/ng-template" id="myRequestsTemplate">
    <div class="panels-container list-group">
			<div ng-show="::!data.is_associated_ticket_tab" class="list-group-item row requests-header-container">
          <div class="col-md-3 col-xs-12 m-b-sm fit-content">
            <div class="form-inline control-view" ng-if="c.options.show_view == 'true'">
            	<label class="control-label hidden-xs wrapper-xs " id="label_view" for="view">${View}</label>
              <select ng-model="c.viewFilter" id="view" class="form-control adjust-width" ng-change="c.changeView()" style="width:80%"
                      ng-options="item.key as item.value for item in c.filterOptions">
              </select>
            </div>
          </div>
          <div class="col-md-4 col-xs-12 padding-left-large fit-content">
            <div class="input-group" style="width:100%">
              <input ng-model="c.filterText" ng-keypress="c.checkEnter($event)"class="form-control" style="width:100%" placeholder="{{data.filterMsg}}" aria-label="{{data.filterMsg}}">
              <span class="input-group-btn">
                <button class="btn btn-default align-icon" type="button" ng-click="c.search()" data-original-title="{{data.filterMsg}}" aria-label="{{data.filterMsg}}" data-toggle="tooltip" data-placement="bottom">
                	<i class="fa fa-search"></i>
                </button>
              </span>
            </div><!-- /input-group -->

          </div>
    	</div>

    	<div ng-if="c.data.request.req_list.length == 0 && !c.filterText" class="panel-body panels-container panel-align">
        ${You do not have any requests} 
      </div>
    	<div ng-if="c.data.request.req_list.length == 0 && c.filterText" class="panel-body panels-container">
        ${Search didn't match any requests} 
      </div>
      <div role="table" ng-if="c.data.request.req_list.length > 0" class="table" aria-label="{{::data.messages.myRequestsTitle}}">
      <div ng-show="::!data.is_associated_ticket_tab" role="rowgroup" class="column-headers">
        <div role="row" class="list-group-item table-responsive">
          <span role="columnheader" class="col-xs-6 padder-r-none padder-l-none">${Request}</span>
          <span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${State}</span>
          <span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${my_requests_updated_capital}</span>
        </div>
   	  </div>
      <div role="rowgroup" class="padder-l-none padder-r-none">
        <div role="row" class="list-group-item table-responsive" ng-repeat="item in c.data.request.req_list | limitTo: c.data.lastLimit track by item.sys_id" style="margin:0px" >
          <div role="cell" class="col-xs-6 padder-l-none padder-r-none main-column">
            <div class="primary-display">
              <a href="?id={{::item.url.id}}&table={{::item.url.table}}&sys_id={{::item.url.sys_id}}" sn-focus="{{::item.highlight}}" aria-label="{{::item.display_field}} , {{::item.display_number}}"> {{::item.display_field}} </a>
            </div>
            <small class="text-muted">
              <div ng-repeat="f in item.secondary_displays" class="secondary-display">
                <span aria-hidden="true">{{::f.display_value}}</span>
              </div>
            </small>
          </div>
          <div role="cell" class="col-xs-3 padder-l-none padder-r-none state-column">
            <div class="state">
              <span tabindex= "0"> {{::item.state}}</span>
            </div>
          </div>
          <div role="cell" class="col-xs-3 padder-l-none padder-r-none updated-column">
            <div class="updated">
              <i class="fa fa-clock-o" aria-hidden="true" title="${Updated}"></i>
              <sn-time-ago timestamp="::item.updated_on"/>
            </div>
          </div>
        </div>
      </div>
    </div>

      <div class="col-sm-12 pull-none" ng-if="c.data.hasMore" style="padding-bottom:15px">
					<div class="text-a-c" ng-if="c.fetching">
          	<i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
						<span class="sr-only">${Loading more requests}</span>  
  	      </div>
          <button class="btn btn-default btn-show-more" ng-click="c.loadMore()"> {{::data.messages.showMoreRequests}} </button>  
      </div>
    
</script>


Client Script as:

function ($scope, $window, spAriaUtil, i18n, spModal) {
  var c = this;
  // MODIFIED Code Starts here
  if (!$window.XLSX) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js";
    script.onload = function () {
      console.log("XLSX loaded dynamically for My Requests");
    };
    document.head.appendChild(script);
  }


  c.downloadMyRequestsXlsx = function () {
    // Export whatever is currently loaded on the page
    var rows = (c.data && c.data.request && c.data.request.req_list) ? c.data.request.req_list : [];

    if (!rows || !rows.length) {
      alert("No data available to export.");
      return;
    }

    if (typeof $window.XLSX === "undefined" || !$window.XLSX) {
      alert("XLSX library is not loaded on this portal page.");
      return;
    }

    // Map My Requests row-shape into export-friendly columns
    var exportData = rows.map(function (item) {
      return {
        "Request": item.display_field || item.display_number || "",
        "State": item.state || "",
        "Updated": item.updated_on || "",
        "Number": item.display_number || item.number || "",
        "Short Description": item.short_description || ""
      };
    });

    var ws = $window.XLSX.utils.json_to_sheet(exportData);
    var wb = $window.XLSX.utils.book_new();
    $window.XLSX.utils.book_append_sheet(wb, ws, "My Requests");

    var ts = new Date().toISOString().slice(0, 10);
    $window.XLSX.writeFile(wb, "My_Requests_" + ts + ".xlsx");
  };


  // MODIFIED Code Ends here

  c.translateDeleteDraftMsg = function (draftItemName) {
    return i18n.getMessage("Delete {0}").withValues([draftItemName]);
  };

  c.trackPage = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "My Requests Widget Loaded");
  };

  if (c.data.is_new_order)
    spAriaUtil.sendLiveMessage(c.data.requestSubmitMsg);

  c.viewFilter = "open";
  c.filterOptions = [
    { key: "open", value: c.data.messages.openRequests },
    { key: "close", value: c.data.messages.closedRequests }
  ];

  c.tabs = [
    { id: "requests", template: "myRequestsTemplate", heading: c.data.messages.requestsTabLabel, active: c.data.isRequestsTabActive },
    { id: "drafts", template: "draftItemsTemplate", heading: c.data.messages.draftsTabLabel, active: c.data.isDraftsTabActive }
  ];

  c.changeView = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "Open/Close Filter Toggled");
    c.server.get({
      action: "change_view",
      view: c.viewFilter,
      search_text: c.filterText
    }).then(function (response) {
      c.data = response.data;
      var resultMsg = (c.data.request.req_list.length === 1 ? "result" : "results") + " returned";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([c.data.request.req_list.length]));
    });
  };

  c.search = function () {
    c.server.get({
      action: "search",
      search_text: c.filterText,
      view: c.viewFilter
    }).then(function (response) {
      c.data = response.data;
      var resultMsg = (c.data.request.req_list.length === 1 ? "result" : "results") + " returned";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([c.data.request.req_list.length]));
    });
  };

  c.checkEnter = function (event) {
    if (event.which === 13)
      c.search();
  };

  c.filterDraftItem = function (item) {
    if (!c.data.draftSearchText)
      return true;

    var searchText = c.data.draftSearchText.toLowerCase();
    return (
      (item.name && item.name.toLowerCase().includes(searchText)) ||
      (item.cart_item_name && item.cart_item_name.toLowerCase().includes(searchText))
    );
  };

  c.updateFilteredDraftItems = function () {
    if (!c.data.draftItems)
      return;

    if (!c.data.draftSearchText)
      c.filteredDraftItems = c.data.draftItems;
    else {
      c.filteredDraftItems = c.data.draftItems.filter(function (item) {
        return c.filterDraftItem(item);
      });
    }
  };

  c.filteredDraftItems = [];
  c.updateFilteredDraftItems();

  c.removeDraftItem = function (cartItem) {
    var options = {
      title: c.data.deleteDraftItemMsg,
      headerStyle: { border: "none", "padding-bottom": 0 },
      footerStyle: { border: "none", "padding-top": 0 },
      messageOnly: true,
      buttons: [
        { label: c.data.dialogCancel, primary: false },
        { label: c.data.dialogDelete, "class": "btn-danger", primary: true }
      ]
    };

    spModal.open(options).then(function (actionButton) {
      if (actionButton) {
        if (!actionButton.primary)
          return;

        c.fetchingDrafts = true;
        c.server.get({
          action: "remove_item",
          removeItemID: cartItem.sys_id,
          prevLimit: (c.data.draftItems && c.data.draftItems.length) ? c.data.draftItems.length : 100,
          attachmentTable: cartItem.attachment_table
        }).then(function (response) {
          c.data.draftItems = response.data.draftItems;
          c.data.hasMoreDrafts = response.data.hasMoreDrafts;
          c.updateFilteredDraftItems();
          c.fetchingDrafts = false;
        });
      }
    });
  };

  c.loadMore = function () {
    window.GlideWebAnalytics.trackEvent("Service Catalog", "Catalog My Requests", "Show More Clicked");
    var currentRequests = c.data.request.req_list.length;
    spAriaUtil.sendLiveMessage("${Loading more requests}");
    c.fetching = true;

    c.server.get({
      action: "fetch_more",
      lastLimit: c.data.lastLimit,
      view: c.viewFilter,
      search_text: c.filterText
    }).then(function (response) {
      var addedRequests = response.data.request.req_list.length - currentRequests;
      var resultMsg = (addedRequests === 1 ? "request" : "requests") + " added";
      spAriaUtil.sendLiveMessage(i18n.getMessage("{0} " + resultMsg).withValues([addedRequests]));
      c.data = response.data;
      c.fetching = false;
    });
  };

  c.loadMoreDrafts = function () {
    c.fetchingDrafts = true;

    c.server.get({
      action: "fetch_more_draft_items",
      prevLimit: (c.data.draftItems && c.data.draftItems.length) ? c.data.draftItems.length : 100
    }).then(function (response) {
      c.data.draftItems = response.data.draftItems;
      c.data.hasMoreDrafts = response.data.hasMoreDrafts;
      c.updateFilteredDraftItems();
      c.fetchingDrafts = false;
    });
  };

  c.changeSelectedTab = function (selectedTab) {
    c.data.isRequestsTabActive = (selectedTab === "requests");
    c.data.isDraftsTabActive = (selectedTab === "drafts");
  };
}

 

Before:

VishalJaswal_4-1778536383370.png

 


After:

VishalJaswal_3-1778536361942.png

 



VishalJaswal_1-1778536343156.png

VishalJaswal_0-1778536566169.png

 






Hope that helps!

Ankur Bawiskar
Tera Patron

@vmanojkumar 

portal is mostly used by end users

So why would you allow end users to export the records?

what's your actual business requirement here?

Regards,
Ankur
✨ Certified Technical Architect  ||  ✨ 10x ServiceNow MVP  ||  ✨ ServiceNow Community Leader

@Ankur Bawiskar 

The widget I was developing is a custom build tailored to the client's request, and they have asked for a feature that allows them to export the data they enter into the tool in .xlsx format.

Tanushree Maiti
Tera Patron

Hi @vmanojkumar 

 

First Refer KB for OOB : KB0824383 It is NOT possible to extract widget data to an Excel 

 

For Custom one, Refer: Custom Widget On Service Portal To Export Table To Excel? 

At client side script of a widget add following code snippet:

window.location.href = "/" + {table name} + .do?EXCEL&sysparm_query=" + {your encoded query} + 
					"&sysparm_fields=" + {field name of the columns from the table};

 

 

 

 

Please Accept the solution if it assisted you with your question & Mark this response as Helpful.
Regards
Tanushree Maiti
ServiceNow Technical Architect
LinkedIn: https://www.linkedin.com/in/tanushreemaiti