NAV

Network Search widget

The Network search widget can be quickly embedded in your internal application using the provided code, reducing your integration and maintenance costs.

Network’s search widget code provides all of the following development tasks that are required for integration:

How does it work?

The Network search widget is supported for internal applications; for example, expense systems, service applications, ERP applications, and event management applications.

Applications must meet the following requirements:

Adding Network’s search widget in your internal application has three steps:

  1. Configure the widget - Network administrators set up the widget in their Network instance. When the configuration is saved, code snippets are generated.
  2. Embed the widget - Web developers embed the generated code snippets into the internal application.

  3. Define optional events:
    select - Web developers define an event handler so that when users select a record the developer is returned the event entity data, which contains a JSON of the selected record. The developer can manipulate the JSON in order to add the record into their internal system.
    add-request-presubmit - Web developers can define an event handler so they can retrieve the fields submitted by the widget user from the add request.
    add-request-submitted - Web developers can define an event handler so they can retrieve the task ID of the submitted add request.

Note: When steps 1 and 2 have been completed, the search widget is fully functional for business users to search, download records, and create add requests to their Network instance.

When all of these steps are complete, the search widget is embedded in your application and users can search for records, view profiles, and add those records to your internal application.

Embed the widget in your internal application

After administrators configure the widget in the Network instance and generate the code, web developers can add the code to your internal application.

There are two blocks of code required for the search widget.

Step 1

Include this code in your HTML page ensuring that the veeva-network-manifest.js script is loaded first.

This part of the code loads the widget.

<script type="text/javascript" src="https://widgets.veevanetwork.com/veeva-network-manifest.js"></script>
<script type="text/javascript" src="https://widgets.veevanetwork.com/veeva-network-search-widget-loader.js"></script>

Step 2

Include this code within the of your HTML page. This part of the code initializes widget.

<veeva-network-search-widget
    widget-name="ServiceCloudA" 
    auth-domain="verteo.veevanetwork.com" 
    widget-id="MTAwMDI7OztzZXJ2aWNlY2xvdWRhX19j">
</veeva-network-search-widget>

The code reflects the values that you defined in the search widget configuration.

HTML Element

<veeva-network-search-widget>

This is the web component for the Network API that allows you to search and download Network data.

Properties

The following required properties are contained in this element:

Note: Properties are written in camelCase, while corresponding attributes are written in kebab-case.

Optional attributes

The following attributes can also be added to the element:

Example

<veeva-network-search-widget
 ...
closeable 
 ...>
<\veeva-network-search-widget>

Example

<veeva-network-search-widget
...
hold-initialization
...>
<\veeva-network-search-widget>

Example

<veeva-network-search-widget
...
preset-filter="hco.hco_type__v=11:98,11:2&hco.hco_status__v=A"
...>
</veeva-network-search-widget>

This preset filter is scoped for the Health Care Organization (HCO) entity using the hco_type__v field. The scope is limited to pharmacies in hospitals (11:2) and other pharmacies (11:98). Administrators can provide the field names and reference codes required for this attribute.

Note: You will need to create a button or link so users access the search widget from a dialog on the page.

Example button code

<veeva-network-search-widget
    id="searchWidget"
    widget-name="ServiceCloudA" 
    auth-domain="verteo.veevanetwork.com" 
    widget-id="MTAwMDI7OztzZXJ2aWNlY2xvdWRhX19j"
    show-in-dialog="true">
  </veeva-network-search-widget>

  <!-- Add the search widget -->
  <div class="container">

    <button id="openWidget">Network Search</button>

  </div>

Note: On some platforms the attribute value is required.

Example

<veeva-network-search-widget
...
show-in-dialog="true"
...>
<\veeva-network-search-widget>

Methods

Use the following methods with the appropriate attributes.

Use with:

Use with:

You can call it multiple times; the content of the widget will only be initialized once.

The close event, veevanetwork:your_widget_name:close-widget, will be read regardless of the value of showInDialog.

Each time the dialog is closed, the widget resets to the first page.

When these two blocks of code have been added to your internal website or application, your business users can access the search widget and search for the most up-to-date records in your Network instance and Veeva OpenData.

If users need to complete a transaction on a record, they can click the Select button beside the record to download it to their Network instance.

Defining custom events

The search widget is completely functional for business users to view, download, and add records when the two provided code blocks from Network have been added to your internal application. However, you can customize the behavior of the widget to meet your business needs.

For example, you can create events so users can download records to your internal application or submit add requests (if they cannot find the record they searched for).

Web developers can add custom code to create an event.

Event requirements

Events available for the search widget are as follows:

Each event requires the following information:

'veeva-network:ServiceCloudA:select'

where:

veeva-network is a predefined value. Do not change.
ServiceCloudA is the name of the widget that was defined in the Network profile widget configuration for this example. Replace this with the name of your search widget.
select is the event name.

Creating select events to download records

You can create an event so users can download records from the search widget to your internal application. By default, if the user clicks the Select button, the record is downloaded to their Network instance. Downloading the record to the internal application enables users to complete transactions on the record; for example, adding the HCP to an expense report.

Required events

One event must be created so users can download records:

Event: select

The select event downloads the record to your downstream system.

Example event name

'veeva-network:ServiceCloudA:select'

Event entity data

The event entity data is provided in JSON format using the following value:

(ev) => {
           const { entity } = ev.detail.entity;
}

where:

For each search result that is downloaded, Network provides a JSON response that includes all of the field values for the entity and the associated child objects (addresses, licenses, parent HCO affiliations).

Use the fields in the JSON response to help you define the requirements for the downloaded record; for example, creating a new record or account in your application.

Example JSON response

{
    "entityId": "505659322685034497",
    "entityType": "HCP",
    "metaData": {},
    "entity": {
        "attribute 1": "Value 1",
        "attribute 2": "Value 2",
        " ... ": " ... ",
        "custom_keys__v": [{
            "attribute 1": "Value 1",
            "attribute 2": "Value 2",
            " ... ": " ... "
        }],
        "addresses__v": [{
            "attribute 1": "Value 1",
            "attribute 2": "Value 2",
            " ... ": " ... ",
            "custom_keys__v": [{
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }]
        }],
        "licenses__v": [{
            "attribute 1": "Value 1",
            "attribute 2": "Value 2",
            " ... ": " ... ",
            "custom_keys__v": [{
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }]
        }],
        "parent_hcos__v": [{
            "attribute 1": "Value 1",
            "attribute 2": "Value 2",
            " ... ": " ... ",
            "custom_keys__v": [{
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }]
        }]
    }
   }

A sample JSON response is provided in the JSON response section below.

For an example of a custom download event, see the Case Study section below.

Creating add request events

The search widget can be configured to allow users to submit add requests when they cannot find the record they searched for. Network administrators must enable the add request option in the search widget configuration in the Network UI.

If this option is enabled, you can create events so users can create records and submit add requests. The add request will be routed to data stewards to validate and process, but users will be able to immediately action the record in the internal application.

Check with your administrator to see if the search widget supports add requests.

Required events

Three events must be created to support add requests:

add-request-presubmit

The add-request-presubmit event provides the task data for the submitted add request.

'veeva-network:ServiceCloudA:add-request-presubmit'

The event entity data for the add-request-presubmit event uses the following value :

(ev) => {
            const { eventdetails } = ev.detail;
 }

where:

Example JSON response

{
  "entity_type": "HCP",
  "metadata": {
    "creator": "Jane.Doe@verteo.veevanetwork.com",
    "system": "ServiceCloud",
    "note": "New HCP",
    "source": "SearchWidget-ServiceCloud"
  },
  "create_unverified": true,
  "entity": {
    "first_name__v": "John",
    "last_name__v": "Smith",
    "hcp_type__v": "P",
    "hcp_status__v": "A",
    "ama_do_not_contact__v": "N",
    "pdrp_optout__v": "N",
    "fellow__v": "N",
    "grad_training__v": "N",
    "primary_country__v": "US",
    "custom_decimal__c": "1.23"
  }
}

add-request-submitted

The add-request-submitted event provides the task ID for the submitted add request.

'veeva-network:ServiceCloudA:add-request-submitted'

The event entity data for the add-request-submitted event uses the following value:

(ev) => {
            const { eventdetails } = ev.detail;
 }

where:

Example JSON response

{taskId: "934001685804876191"}

select event

Use the same select event from the Creating download events section above.

JSON response

For each search result that is downloaded, Network provides a JSON response that includes all of the field values for the entity and the associated child objects (addresses, licenses, parent HCO affiliations). The JSON is the first object in the entities array returned from the Network Retrieve Entity API and is available using ev.detail.entity in the download event object.

Example JSON response

Note that the field values in this response use the reference aliases of the source system that was chosen during the search widget configuration.

{
 "entityId": "242979976201110533",
 "entityType": "HCP",
 "metaData": {},
 "entity": {
    "gender__v": "Male",
    "specialty_1__v": "Prescriber",
    "hcp_type__v": "N",
    "master_vid__v": "242979976201110533",
    "first_name__v": "John",
    "record_owner_type__v": "VOD",
    "grad_training__v": "N",
    "record_delta_id__v": "930473391097610239",
    "last_name__v": "Jones",
    "sha_id__v": "209784225",
    "record_owner_name__v": "OpenData",
    "medical_degree_1__v": "RN",
    "fellow__v": "N",
    "kaiser__v": "N",
    "formatted_name__v": "John Jones",
    "primary_country__v": "United States",
    "created_date__v": "2017-04-28T16:12:59.000-05:00",
    "pdrp_optout__v": "N",
    "is_veeva_master__v": true,
    "hcp_status__v": "A",
    "ama_do_not_contact__v": "N",
    "status_update_time__v": "2017-04-28T16:12:59.000-05:00",
    "ams_id__v": "15873366",
    "modified_date__v": "2017-05-16T15:39:13.000-05:00",
    "record_state__v": "VALID",
    "verteo_id__c": "VSSW-0GE-TH3",
    "record_version__v": 0,
    "vid__v": "242979976201110533",
    "is_externally_mastered__v": false,
    "specialty_1_rank__v": 1,
    "custom_keys__v": [{
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "Account",
        "custom_key_value__v": "001V000000PXmbTIAT",
        "modified_date__v": "2017-04-28T16:13:07.000-05:00",
        "vid__v": "930253893769625631",
        "custom_key_vid_key__v": "VCRM-00DV0000006tHq7MAE:Account:001V000000PXmbTIAT",
        "custom_key_source_type__v": "VCRM-00DV0000006tHq7MAE",
        "created_date__v": "2017-04-28T16:13:07.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-04-28T16:13:07.000-05:00"
    }, {
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "Account",
        "custom_key_value__v": "001Q0000016Lp9SIAS",
        "modified_date__v": "2017-05-12T18:00:30.000-05:00",
        "vid__v": "930333588319567903",
        "custom_key_vid_key__v": "VCRM-00DQ000000GKl1iMAD:Account:001Q0000016Lp9SIAS",
        "custom_key_source_type__v": "VCRM-00DQ000000GKl1iMAD",
        "created_date__v": "2017-05-12T18:00:30.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-05-12T18:00:30.000-05:00"
    }, {
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "Account",
        "custom_key_value__v": "001W000000RjMBZIA3",
        "modified_date__v": "2017-05-15T20:12:00.000-05:00",
        "vid__v": "930351092316766239",
        "custom_key_vid_key__v": "CRM:Account:001W000000RjMBZIA3",
        "custom_key_source_type__v": "CRM",
        "created_date__v": "2017-05-15T20:12:00.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-05-15T20:12:00.000-05:00"
    }, {
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "Account",
        "custom_key_value__v": "001W000000RcjsvIAB",
        "modified_date__v": "2017-05-16T15:39:13.000-05:00",
        "vid__v": "930355682040283167",
        "custom_key_vid_key__v": "VCRM-00DW0000008xGqaMAE:Account:001W000000RcjsvIAB",
        "custom_key_source_type__v": "VCRM-00DW0000008xGqaMAE",
        "created_date__v": "2017-05-16T15:39:13.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-05-16T15:39:13.000-05:00"
    }, {
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "HCP",
        "custom_key_value__v": "242979976201110533",
        "modified_date__v": "2017-04-28T16:13:00.000-05:00",
        "vid__v": "930253893256609824",
        "custom_key_vid_key__v": "MASTER__v:HCP:242979976201110533",
        "custom_key_source_type__v": "MASTER__v",
        "created_date__v": "2017-04-28T16:13:00.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-04-28T16:13:00.000-05:00"
    }, {
        "custom_key_entity_id__v": "242979976201110533",
        "custom_key_item_type__v": "Account",
        "custom_key_value__v": "001W000000RU9wdIAD",
        "modified_date__v": "2017-05-15T15:36:08.000-05:00",
        "vid__v": "930350007605657631",
        "custom_key_vid_key__v": "VCRM-00DW0000008xHNsMAM:Account:001W000000RU9wdIAD",
        "custom_key_source_type__v": "VCRM-00DW0000008xHNsMAM",
        "created_date__v": "2017-05-15T15:36:08.000-05:00",
        "custom_key_entity_type__v": "HCP",
        "custom_key_status__v": "A",
        "status_update_time__v": "2017-05-15T15:36:08.000-05:00"
    }],
    "licenses__v": [{
        "record_owner_type__v": "VOD",
        "best_state_license__v": "Y",
        "grace_period__v": "2016-04-30",
        "license_eligibility__v": "I",
        "record_delta_id__v": "930253893279973376",
        "anticipated_expiry_date__v": "2016-10-27",
        "record_owner_name__v": "OpenData",
        "expiration_date__v": "2016-04-30",
        "rxa_eligible__v": "I",
        "license_status_condition__v": "0",
        "dea_business_activity_code__v": "ZZ",
        "created_date__v": "2017-04-28T16:13:00.000-05:00",
        "is_veeva_master__v": true,
        "type_value__v": "PA",
        "license_degree__v": "RN",
        "effective_date__v": "1977-03-21",
        "license_number__v": "RN215307L",
        "status_update_time__v": "2017-04-28T16:13:00.000-05:00",
        "license_status__v": "A",
        "entity_type__v": "HCP",
        "body__v": "BON",
        "modified_date__v": "2017-04-28T16:13:00.000-05:00",
        "initial_board_license_date__v": "2014-06-20",
        "record_state__v": "VALID",
        "license_subtype__v": "U",
        "vid__v": "243244039892108289",
        "entity_vid__v": "242979976201110533",
        "is_externally_mastered__v": false,
        "type__v": "STATE",
        "custom_keys__v": [{
            "custom_key_entity_id__v": "243244039892108289",
            "custom_key_item_type__v": "LICENSE",
            "custom_key_value__v": "243244039892108289",
            "modified_date__v": "2017-04-28T16:13:00.000-05:00",
            "vid__v": "930253893256609823",
            "custom_key_vid_key__v": "MASTER__v:LICENSE:243244039892108289",
            "custom_key_source_type__v": "MASTER__v",
            "created_date__v": "2017-04-28T16:13:00.000-05:00",
            "custom_key_entity_type__v": "LICENSE",
            "custom_key_status__v": "A",
            "status_update_time__v": "2017-04-28T16:13:00.000-05:00"
        }]
    }],
    "addresses__v": [{
        "dpv_confirmed_indicator__v": "X",
        "nwk_primary_address__c": "Y",
        "postal_code_primary__v": "06810",
        "address_line_1__v": "24 Hospital Ave",
        "record_owner_type__v": "VOD",
        "premise__v": "24",
        "cbsa__v": "25420",
        "record_delta_id__v": "930253893783617535",
        "record_owner_name__v": "OpenData",
        "thoroughfare_trailing_type__v": "Ln",
        "locality__v": "Danbury",
        "delivery_address__v": "24 Hospital Ave",
        "country__v": "United States",
        "created_date__v": "2017-04-28T16:13:00.000-05:00",
        "premise_number__v": "24",
        "is_veeva_master__v": true,
        "thoroughfare__v": "Hospital Ave",
        "status_update_time__v": "2017-04-28T16:13:00.000-05:00",
        "address_type__v": "U",
        "delivery_address_1__v": "24 Hospital Ave",
        "sub_administrative_area__v": "Cumberland",
        "entity_type__v": "HCP",
        "address_verification_status__v": "V",
        "address_status__v": "A",
        "modified_date__v": "2017-04-28T16:13:07.000-05:00",
        "record_state__v": "VALID",
        "vid__v": "243244039892108291",
        "entity_vid__v": "242979976201110533",
        "postal_code__v": "06810-6099",
        "is_externally_mastered__v": false,
        "administrative_area__v": "Connecticut",
        "formatted_address__v": "24 Hospital Ave Connecticut 06810-6099 United States",
        "latitude__v": 40.261007,
        "address_ordinal__v": 1,
        "thoroughfare_name__v": "Hospital",
        "postal_code_secondary__v": "6099",
        "longitude__v": -76.963044,
        "custom_keys__v": [{
            "custom_key_entity_id__v": "243244039892108291",
            "custom_key_item_type__v": "ADDRESS",
            "custom_key_value__v": "243244039892108291",
            "modified_date__v": "2017-04-28T16:13:00.000-05:00",
            "vid__v": "930253893255495711",
            "custom_key_vid_key__v": "MASTER__v:ADDRESS:243244039892108291",
            "custom_key_source_type__v": "MASTER__v",
            "created_date__v": "2017-04-28T16:13:00.000-05:00",
            "custom_key_entity_type__v": "ADDRESS",
            "custom_key_status__v": "A",
            "status_update_time__v": "2017-04-28T16:13:00.000-05:00"
        }, {
            "custom_key_entity_id__v": "243244039892108291",
            "custom_key_item_type__v": "Address_vod__c",
            "custom_key_value__v": "a01V0000005MgywIAC",
            "modified_date__v": "2017-04-28T16:13:08.000-05:00",
            "vid__v": "930253893783519263",
            "custom_key_vid_key__v": "VCRM-00DV0000006tHq7MAE:Address_vod__c:a01V0000005MgywIAC",
            "custom_key_source_type__v": "VCRM-00DV0000006tHq7MAE",
            "created_date__v": "2017-04-28T16:13:08.000-05:00",
            "custom_key_entity_type__v": "ADDRESS",
            "custom_key_status__v": "A",
            "status_update_time__v": "2017-04-28T16:13:08.000-05:00"
        }]
    }],
    "parent_hcos__v": [],
    "research_set__c": [],
    "training_set__c": []
 }
 }

Opening the search widget in a popup

Web developers can configure the search widget so it opens in a popup window in their internal application instead of directly on the page.

Example popup code

Review the example code to see how this option can be configured.

<apex:page standardController="Account">

  <!-- Include the script to load widget -->
  <script type="text/javascript" src="https://widgets.veevanetwork.com/veeva-network-manifest.js"></script>
  <script type="text/javascript" src="https://widgets.veevanetwork.com/veeva-network-search-widget-loader.js"></script>

  <!-- These are the styles for the visualforce page. Put them in a custom-style to ensure no styles leak to the widget -->
  <custom-style>
    <style>
      .container {
        text-align: center;
      }
    </style>
  </custom-style>
  <!--
    Turn on popup mode:
      set show-in-dialog attribute to true or just add show-in-dialog attribute without setting a value.
      Note some environments (e.g. salesforce) require html attribute to have a value.
    Turn off popup mode:
      remove the show-in-dialog attribute.
  -->
  <veeva-network-search-widget
    id="searchWidget"
    widget-name="ServiceCloud"
    auth-domain="verteo.veevanetwork.com"
    widget-id="MTAwMDI70ztzZXJ2aWNLY2xvdWRhX19j"
    show-in-dialog="true">
  </veeva-network-search-widget>

  <!-- Add the search widget -->
  <div class="container">
    <button id="openWidget">open widget</button>
  </div>

  <script>
    const searchWidget = document.getElementById('searchWidget');
    const btn = document.getElementById('openWidget');
    btn.addEventListener('click', function () {
      searchWidget.start();
    });
  </script>

</apex:page>

Case Study - Creating a download event in Service Cloud™

This case study provides the example code used to create a download event that adds an account to Salesforce’s Service Cloud application. There are many different ways that a download event can be created for a platform; this is just one example to display the capability of the search widget.

Scenario

The search widget has been embedded in Salesforce’s Service Cloud application. A call center agent, Fernando Lopes, searches for an HCP named John Jones. The widget searches the records in Fernando’s Network instance and Veeva OpenData and displays the results. He finds John Jones in the search results and reviews the record details to ensure that this is the correct HCP.

Fernando clicks Select Account to download the record to his Network instance and create a Service Cloud account for John Jones.

Solution

For this scenario, Joseph, a Service Cloud system integrator, must add custom HTML code to create the download event so a Service Cloud account can be created for the HCP record.

Within the download event code, Joseph adds code to address the following requirements:

a. Salesforce remote object - The event must contain the Salesforce remote object so an account and its linked address can be created.
b. Field mappings - Map the Network fields in the search result’s JSON response to the Service Cloud fields.
c. Address - Link the address child object to the account (so that the Addresses section displays).
d. Error message handling - Create error messages for the download event (failures and duplicates) so users have feedback if errors occurs.

On the Service Cloud page that contains the Network Search widget code, Joseph adds custom code to address all of the requirements.

The sections below detail each part of the code that is added. To review the entire HTML source (with comments) for the Service Cloud page that contains the search widget, see the “Search widget source code” section below.

Add the download event

Joseph adds the code to receive the download event within a <script> tag.

The event name is in the following format:

'veeva-network:<ServiceCloudA>:select'

where:

const registerListeners = () => {

const searchWidget = document.getElementById('searchWidget');

// Listen download entity event on search widget
searchWidget.addEventListener(

// The event name format is: veeva-network:your-widget-name:select
// Replace <your-widget-name> with the widget name.
'veeva-network:ServiceCloudA:select',
// This is the event handler. 
(ev) => {
  // The record data is in the event detail object.
  // ev.detail.entity has the same format as the entity
  // object format in the Network API retrieve entity call
  const { entity } = ev.detail.entity;
  //This is the field mapping between Service Cloud and Network. 
  //The format is Service Cloud field name: Network field name.
  const account = {
    [FirstName: entity.first_name__v,
        LastName: entity.last_name__v,
        PersonEmail: entity.email_1__v,
        Specialty_1_vod__c: entity.specialty_1__v,
        NET_External_Id__c: entity.vid__v,
        Gender_vod__c: entity.gender__v]
  };
  // Declare a remote salesforce account object
  const salesforceAccount = new SObjectModel.Account();
  // Calling create method to create a new account in Salesforce
  // using the information we have from the field mapping above.
  salesforceAccount.create(account, (err, results, ev) => {
    // If results is not empty, the salesforce account was created.
    if (results && results.length > 0) {
      // Now start working on addresses. Here we use Promise to
      // defer the redirection to account page until we successfully
      // create all the addresses on salesforce
      new Promise((resolve, reject) => {
        // Get addresses from the downloaded record
        const addresses = entity.addresses__v;
        // If the record has addresses
        if (Array.isArray(addresses)) {
          // This is the count of calling create method 
          //on remote address object.
          // We will use it later to determine that we have finished 
          //iteration of the addresses and their creations.
          let count = 0;
          // Iterate addresses
          addresses.forEach((addr) => {
            //Address field mapping
            const address = {
              // This is where the address is linked to the account.
              // results[0] is the ID of the account
              // that we just created on salesforce
              Account_vod__c: results[0],
              Name: addr.address_line_1__v,
              Address_line_2_vod__c: addr.address_line_2__v,
              City_vod__c: addr.locality__v,
              State_vod__c: addr.administrative_area__v,
              Country_vod__c: addr.country__v
            };
            // Declare a remote salesforce address object
            const salesforceAddress = new SObjectModel.Address_vod__c();
            // Calling create method to create a new address on the
            // account we just created in salesforce with the information we
            // have from the address field mapping above.
            salesforceAddress.create(address, (addrErr) => {
              // Add one to the count once the creation call responds
              count += 1;
              // if there is an error, log it in the console
              if (addrErr) {
                console.error(addrErr.message + '\n', addrErr);
              }
              // if we have finished all the creations of the addresses
              // resolve the Promise to go to the next step
              if (count === addresses.length) {
                resolve();
              }
            });
          });
        }
      })
      // After addresses are created, go to account detail.
      .then(() => sforce.one.navigateToSObject(results[0]));
    //Error handling starts here.
    } else {
      // Error: failed to create salesforce account from downloaded record.
      // Log the error into console
      console.error(err.message + '\n', err);
      // Check if the error is because a duplicate value was found.
      const match = err.message.match(/^duplicate value found: .* with id: (.+)$/i);
      if (match) {
        // If it's duplicate error, redirect to the existing account.
        sforce.one.navigateToSObject(match[1]);
      }
    }
  });
}

);

};

if (document.attachEvent ? document.readyState === 'completed' : document.readyState !== 'loading') {
  registerListeners();
} else {
  document.addEventListener('DOMContentLoaded', registerListeners);

Include the Salesforce remote object

In addition to the download event, the HTML file must include the Salesforce remote object so a new account and its linked address can be created in the Service Cloud application. For more information about the Salesforce remote object, see the Salesforce documentation.

Joseph adds the remote objects, Account and Address_vod__c, to the HTML before the code where he receives the download event.

<apex:remoteObjects >
    <apex:remoteObjectModel name="Account"></apex:remoteObjectModel>
    <apex:remoteObjectModel name="Address_vod__c"></apex:remoteObjectModel>
  </apex:remoteObjects>

Define the field mapping

During the handling of the download event, the Network field names can be mapped to the Service Cloud field names so that the values in the account are accurate. The mapping format is: Service Cloud field name: Network field name. Within the download event code, Joseph adds the following code to map the field names following the event detail object, ev.detail.entity.

const account = {
    FirstName: entity.first_name__v,
    LastName: entity.last_name__v,
    PersonEmail: entity.email_1__v,
    Specialty_1_vod__c: entity.specialty_1__v,
    NET_External_Id__c: entity.vid__v,
    Gender_vod__c: entity.gender__v
          };

To ensure that the Service Cloud account has the address child object linked to it, Joseph adds code in the address iteration when handling the download event.

const address = {            
   Account_vod__c: results[0],
   Name: addr.address_line_1__v,
   Address_line_2_vod__c: addr.address_line_2__v,
   City_vod__c: addr.locality__v,
   State_vod__c: addr.administrative_area__v,
   Country_vod__c: addr.country__v
       };

results[0] is the ID of the account that is being created in Service Cloud by the event code.

Error handling

Joseph adds error handling so business users have feedback if an issue occurs during the download event.

The else block handles the error when there is a failure trying to create a Salesforce account. The results variable provided in the callback of the create account API contains the information of the account created. If this variable is empty, then it means there is an error.

Secondly, Joseph identifies if it is a duplicate account error. If a business user downloads a record that has an existing account, the user will be automatically redirected to the existing account.

} else {
     console.error(err.message + '\n', err);
    // Check if the error is because a duplicate value was found.
    const match = err.message.match(/^duplicate value found: .* with id:           (.+)$/i);
      if (match) {
        // If it's duplicate error, redirect to the existing account.
                sforce.one.navigateToSObject(match[1]);
              }
            }

With the new download event defined, whenever a business user clicks the Select button an account will be created in Service Cloud.

Search widget integration code

The entire code for the Service Cloud page that includes the search widget is defined below.

Note: The code includes comments to help you navigate through each code instruction and a tag that contains the existing style for the Service Cloud page.

If you use the code from this section, ensure that you update any values to reflect your specific search widget.

<apex:page standardController="Account">

    <!-- Include the script to load widget -->

    <script type="text/javascript" src="https://widgets.veevanetwork.com/dev/veeva-network-manifest.js"></script>
    <script type="text/javascript" src="https://widgets.veevanetwork.com/dev/veeva-network-search-widget-loader.js"></script>

    <!-- Include the Salesforce remote object so a new account and its linked address can be created.
    More information: https://developer.salesforce.com/docs/atlas.en-us.pages.meta/pages/pages_remote_objects_using.htm
  -->

  <apex:remoteObjects >
    <apex:remoteObjectModel name="Account"></apex:remoteObjectModel>
    <apex:remoteObjectModel name="Address_vod__c"></apex:remoteObjectModel>
  </apex:remoteObjects>


  <!-- Add the search widget -->
  <div class="container">

    <veeva-network-search-widget
      id="searchWidget"
      widget-name="ServiceCloudA" 
      auth-domain="verteo.veevanetwork.com" 
      widget-id="MTAwMDI7OztzZXJ2aWNlY2xvdWRhX19j">
    </veeva-network-search-widget>

  </div>

<!-- Add the event to download the record to Service Cloud and create a new account -->

  <script>
    const registerListeners = () => {
      const searchWidget = document.getElementById('searchWidget');
      // listen download entity event on search widget
      searchWidget.addEventListener(
        // The event name format is: veeva-network:your-widget-name:select
        // replace <your-widget-name> with your widget name.
        'veeva-network:ServiceCloudA:select',
        // This is the event handler. 
        (ev) => {
          // The record data is in the event detail object.
          // ev.detail.entity has the same format as the entity
          // object format in the Veeva Network API retrieve entity call
          const { entity } = ev.detail.entity;

          // This is the field mapping between Service Cloud and Network. 
          // The format is: Service Cloud field name: Network field name.
          const account = {
            FirstName: entity.first_name__v,
            LastName: entity.last_name__v,
            PersonEmail: entity.email_1__v,
            Specialty_1_vod__c: entity.specialty_1__v,
            NET_External_Id__c: entity.vid__v,
            Gender_vod__c: entity.gender__v
          };
          // Declare a remote salesforce account object
          const salesforceAccount = new SObjectModel.Account();
          // Calling create method to create new Salesforce account
          // using the information we have from the field mapping above.
          salesforceAccount.create(account, (err, results, ev) => {
            // If results is not empty, we created salesforce account.
            if (results && results.length > 0) {
              // Now start working on addresses. Here we use Promise to
              // defer the redirection to account page until we successfully
              // create all the addresses on salesforce
              new Promise((resolve, reject) => {
                // Get addresses from the downloaded record
                const addresses = entity.addresses__v;
                // If the record has addresses
                if (Array.isArray(addresses)) {
                  // This is the count of calling create method 
                  // on remote address object.
                  // We will use it later to determine that we have finished 
                  // iteration of the addresses and their creations.
                  let count = 0;
                  // Iterate addresses
                  addresses.forEach((addr) => {
                    //Address field mapping
                    const address = {
                      // This is where the address is linked to the account.
                      // results[0] is the ID of new Salesforce account.
                      Account_vod__c: results[0],
                      Name: addr.address_line_1__v,
                      Address_line_2_vod__c: addr.address_line_2__v,
                      City_vod__c: addr.locality__v,
                      State_vod__c: addr.administrative_area__v,
                      Country_vod__c: addr.country__v
                    };
                    // Declare a remote salesforce address object
                    const salesforceAddress = new SObjectModel.Address_vod__c();
                    // Calling create method to create a new address on the
                    // account we just created in salesforce with the 
                    // info from the address field mapping above.
                    salesforceAddress.create(address, (addrErr) => {
                      // Add one to the count after create call responds.
                      count += 1;
                      // if there is an error, log it in the console
                      if (addrErr) {
                        console.error(addrErr.message + '\n', addrErr);
                      }
                      // If we have finished creating all addresses,
                      // resolve the Promise to go to next step
                      if (count === addresses.length) {
                        resolve();
                      }
                    });
                  });
                }
              })
              // After addresses are created, go to account detail.
              .then(() => sforce.one.navigateToSObject(results[0]));
            //Error handling starts here.
            } else {
              // Error: failed to create salesforce account from 
              // the downloaded record.
              // Log the error into console
              console.error(err.message + '\n', err);
              // Check if the error is because a duplicate value was found.
              const match = err.message.match(/^duplicate value found: .* with id: (.+)$/i);
              if (match) {
                // If duplicate error, redirect to existing account.
                sforce.one.navigateToSObject(match[1]);
              }
            }
          });
        }
      );
    };

    if (document.attachEvent ? document.readyState === 'completed' : document.readyState !== 'loading') {
      registerListeners();
    } else {
      document.addEventListener('DOMContentLoaded', registerListeners);
    }
  </script>

</apex:page>

Network Profile widget

Network’s profile widget code provides all of the following development tasks that are required for integration:

The profile widget can also be configured to support edit-mode to enable users to update records and submit data change requests.

How does it work?

The Network profile widget is supported for internal applications.

Administrators configure the widget settings in Network and then web developers add the generated code to their internal application. Then, from that application, users can open a profile and view Network’s latest data for an HCO or HCP. Only profiles that users have access to through their assigned Network data visibility profile will display.

Applications must meet the following requirements:

Adding Network’s profile widget in your internal application has three or four steps depending on whether the widget is read-only or supports edit mode:

  1. Configure the widget - Network administrators set up the widget in their Network instance. When the configuration is saved, code snippets are generated.
  2. Embed the widget - Web developers embed the generated code snippets into the portal.
  3. Additional configuration - Web developers initialize the widget by opening records using Network entity IDs (VIDs) or custom keys.
  4. Define events to support edit-mode – If administrators have configured the profile widget to allow users to edit records and submit change requests, web developers must add events to the profile widget to get the latest copy of the record (including new under review records) and to retrieve the task ID or the task details.

Embed the widget

After administrators configure the widget in the Network instance and generate the code, web developers can add the code to an internal web-based application.

Add the blocks of code that were generated when the administrator configured the profile widget in the Network instance.

There are two blocks of code required for the profile widget:

Step 1

Include this code in your HTML page ensuring that the veeva-networkmanifest.js script is loaded first.

This part of the code loads the widget.

<script type="text/javascript" src="https://widgets.veevanetwork.com/veeva-networkmanifest.js"></script>
<script type="text/javascript" src="widgets.veevanetwork.com/veevanetwork-profile-widget-loader.js"></script>

Step 2

Include this code within the of your HTML page. This part of the code initializes widget.

<veeva-network-profile-widget
    widget-name="ProfileWidget"
    auth-domain="verteo.veevanetwork.com"
    widget-id="MTAwMDI7Oztwcm9maWxld2lkZ2V0X19j">
</veeva-network-profile-widget>

The code reflects the settings that were defined in the profile widget configuration.

Initialize the widget

Initialize the widget using the start method with the Network entity ID. For more information, see the Methods section and the implementation example below.

When the two blocks of code and the start function (to initialize the widget) have been added to your HTML, your business users can view the latest data from your Network instance in this internal application.

Developer reference

This section includes the HTML element, attributes, and methods that can be used when you add the profile widget to your internal web-based application.

HTML Element

veeva-network-profile-widget

This is the web component for the Network API that allows you to view the record profile. To use it, simply treat it as an HTML element and add it to your HTML.

Properties

The following required properties are contained in this element:

Note: Properties are written in camelCase, while corresponding attributes are written in kebab-case.

<veeva-network-profile-widget
    widget-name="your-widget-name"
    auth-domain="your-auth-domain"
    widget-id="your-generated-widget-id">
</veeva-network-profile-widget>

Optional attributes

The following attributes can also be added to the element:

Example

<veeva-network-profile-widget
...
closeable 
...>
<\veeva-network-profile-widget>

Example

<veeva-network-profile-widget
...
hold-initialization
...>
<\veeva-network-profile-widget>

Example

<veeva-network-profile-widget
    widget-name="your-widget-name"
    auth-domain="your-auth-domain"
    widget-id="your-generated-widget-id"
    identifier= “your-network-vid”>
</veeva-network-profile-widget>

Note: You will need to create a button or link so users access the profile widget from a dialog on the page.

Example button code

<veeva-network-profile-widget
    ...
    show-in-dialog
    ...>
</veeva-network-profile-widget>

Note: On some platforms the attribute value is required.

Example

<veeva-network-profile-widget
    ...
    show-in-dialog="true"
    ...>
</veeva-network-profile-widget>

Methods

Use the following methods with the appropriate attributes.

Use with:

Use with:

The close event, veevanetwork:your_widget_name:close-widget, will be read regardless of the value of showInDialog.

Implementation example

Web developers determine how the profile widget can be accessed in their internal application; for example, the profile widget could be linked to the HCP or HCO name in a portal. When the user clicks the name, the profile widget is accessed and the record profile displays.

When a user performs the action to open the profile widget, it calls the start function.

profileWidget.start(document.getElementById('networkVid').value)

Example

In this example, a web developer is configuring the profile widget to open when a user clicks the name of an HCP or HCO in their internal portal.

// For example, you have a list of entries (yourEntryList) with entity id in your UI.
    // You can add a click event listener to your list.
    yourEntryList.addEventListener('click', function (ev) {
        const target = ev.target;
        // then when the click event pops up to the list, you can check if
        // the event target is the trigger element of the profile widget,
        // for example, a name or an ID
        if (isProfileWidgetTrigger(target)) {
            // If it is the profile widget trigger element, then you can
            // trigger the profile widget to open a profile by calling the start
            // function on the profile widget element with the entity id
            // you get from your target element (here we assume you
            // already have the reference of the profile widget element).
            profileWidgetElement.start(getEntityId(target));
        }
    });

Define events to support edit mode

The profile widget is completely functional for business users to view records when the two provided code blocks generated by Network have been added to your internal application.

Events to support edit-mode need to be created only if administrators have enabled the option in the profile widget configuration that allows users to submit data change requests. Check with your Network administrator to see if the profile widget supports data change requests.

Data change request events

Three events must be created to support data change requests:

Event requirements

Events require the following information:

'veeva-network:ServiceCloudA:dcr-submitted'

where:

veeva-network is a predefined value. Do not change.
ServiceCloudA is the name of the widget that was defined in the Network profile widget configuration for this example. Replace this with the name of your profile widget.
dcr-submitted is the event name.

Event: dcr-presubmit

The dcr-presubmit event provides the task data for the changes that are submitted.

Example event name

veeva-network:ServiceCloudA:dcr-presubmit'

Event entity data

The event entity data for the dcr-presubmit event uses the following value:

(ev) => {
            const { eventdetails } = ev.detail;
}

where:

Network provides a JSON response that includes all of the details for the change request, including the creator, requester notes, and all of the previous and new field values for the record and the associated child objects (addresses, licenses, parent HCO affiliations).

Example JSON response

{
    "metadata": {
        "creator": "linda.wong@verteo.com",
        "system": "ProfileWidget",
        "note": "John Smith is a new doctor in the Network Hospital",
        "source": "ProfileWidget-ServiceCloudProfile",
        "system": "ServiceCloud"
    },
    "entity_type": "HCP",
    "vid_key": "Network:HCP:214066274852930560",
    "entity": {
        "attribute 1": "Value 1",
        "attribute 2": "Value 2",
        " ... ": " ... ",
        "addresses__v": [
            {
                "vid_key": "Network:Address:214066274852930560",
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }
        ],
        "licenses__v": [
            {
                "vid_key": "Network:License:340010313220883456",
                "address_vid_key": "Network:Address:26090478086232576",
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }
        ],
        "parent_hcos__v": [
            {
                "vid_key": "Network:ParentHCO:340010217892742157",
                "parent_hco_vid_key": "Network:HCO:215249678634058752",
                "attribute 1": "Value 1",
                "attribute 2": "Value 2",
                " ... ": " ... "
            }
        ]
    }
}

Event: dcr-submitted

The dcr-submitted event provides the task ID for the changes that are submitted.

Example event name

'veeva-network:ServiceCloudA:dcr-submitted'

Event entity data

The event entity data for the dcr-submitted event uses the following value:

(ev) => {
            const { eventdetails } = ev.detail;
}

where:

Network provides a JSON response that includes the task ID for the change request.

Example JSON response

{taskId: "934001685804876191"}

Event: select

The select event provides the latest copy of the record from the Network instance (including any new sub-objects that the user added in the change request).

Example event name

'veeva-network:ServiceCloudA:select'

Event entity data

The event entity data for the select event uses the following value:

(ev) => {
            const { entity } = ev.detail.entity;
}

where:

Network provides a JSON response that includes all of the field values for the entity and the associated child objects (addresses, licenses, parent HCO affiliations).

Example JSON

{
    "entityId": "934001011900023167",
    "entityType": "HCP",
    "metaData": {},
    "entity": {
        "specialty_1__v": "Acute Care",
        "hcp_type__v": "P",
        "first_name__v": "David",
        "record_owner_type__v": "LOCAL",
        "grad_training__v": "N",
        "npi_num__v": "052005210521051",
        "record_delta_id__v": "934001072423731199",
        "last_name__v": "Smith",
        "record_owner_name__v": "Local",
        "fellow__v": "N",
        "kaiser__v": "N",
        "formatted_name__v": "David Smith",
        "primary_country__v": "US",
        "created_date__v": "2019-02-19T15:34:34.000Z",
        "pdrp_optout__v": "N",
        "is_veeva_master__v": false,
        "hcp_status__v": "A",
        "ama_do_not_contact__v": "N",
        "status_update_time__v": "2019-02-19T15:34:34.000Z",
        "modified_date__v": "2019-02-19T15:49:55.000Z",
        "verteo_id__c": "V-M9120V15BZ",
        "record_state__v": "UNDER_REVIEW",
        "global_target__c": 0,
        "record_version__v": 1,
        "vid__v": "934001011900023167",
        "is_externally_mastered__v": false,
        "specialty_1_rank__v": 1,
        "custom_keys__v": [{
                "custom_key_entity_id__v":
                "934001011900023167",
                "custom_key_item_type__v": "HCP",
                "custom_key_value__v": "934001011900023167",
                "modified_date__v": "2019-02-19T15:34:34.000Z",
                "vid__v": "934001011995181439",
                "custom_key_vid_key__v":
                "CUSTOMER__v/3/change_request:HCP:934001011900023167",
                "custom_key_source_type__v":
                "CUSTOMER__v/3/change_request",
                "created_date__v": "2019-02-19T15:34:34.000Z",
                "custom_key_entity_type__v": "HCP",
                "custom_key_status__v": "A",
                "status_update_time__v": "2019-02-19T15:34:34.000Z"
            }, {
                "custom_key_entity_id__v":
                "934001011900023167",
                "custom_key_item_type__v": "DCRID__v",
                "custom_key_value__v": "934001011901333919",
                "modified_date__v": "2019-02-19T15:34:34.000Z",
                "vid__v": "934001011995443583",
                "custom_key_vid_key__v":
                "WORKFLOW__v:DCRID__v:934001011901333919",
                "custom_key_source_type__v": "WORKFLOW__v",
                "created_date__v": "2019-02-19T15:34:34.000Z",
                "custom_key_entity_type__v": "HCP",
                "custom_key_status__v": "A",
                "status_update_time__v": "2019-02-19T15:34:34.000Z"
            }],
        "licenses__v": [],
        "addresses__v": [{
                "address_line_1__v": "30 York Street",
                "record_owner_type__v": "LOCAL",
                "record_delta_id__v": "934001072381526016",
                "record_owner_name__v": "Local",
                "locality__v": "New York",
                "country__v": "US",
                "created_date__v": "2019-02-19T15:49:55.000Z",
                "network_primary__c": "N",
                "is_veeva_master__v": false,
                "status_update_time__v": "2019-02-19T15:49:55.000Z",
                "address_type__v": "P",
                "entity_type__v": "HCP",
                "address_verification_status__v": "U",
                "address_status__v": "A",
                "modified_date__v": "2019-02-19T15:49:55.000Z",
                "record_state__v": "UNDER_REVIEW",
                "vid__v": "934001072372056448",
                "entity_vid__v": "934001011900023167",
                "postal_code__v": "1004",
                "administrative_area__v": "New York",
                "formatted_address__v": "30 York Street New York US-NY 1004",
                "address_ordinal__v": 2,
                "custom_keys__v": []
            }, {
                "address_line_1__v": "40 York Street",
                "record_owner_type__v": "LOCAL",
                "record_delta_id__v": "934001012009304064",
                "record_owner_name__v": "Local",
                "locality__v": "New York",
                "country__v": "US",
                "created_date__v": "2019-02-19T15:34:34.000Z",
                "network_primary__c": "Y",
                "is_veeva_master__v": false,
                "status_update_time__v": "2019-02-19T15:34:34.000Z",
                "address_type__v": "P",
                "entity_type__v": "HCP",
                "address_verification_status__v": "U",
                "address_status__v": "A",
                "modified_date__v": "2019-02-19T15:34:34.000Z",
                "record_state__v": "UNDER_REVIEW",
                "vid__v": "934001011986465152",
                "entity_vid__v": "934001011900023167",
                "postal_code__v": "10004",
                "administrative_area__v": "New York",
                "formatted_address__v": "40 York Street New York US-NY 10004",
                "address_ordinal__v": 1,
                "custom_keys__v": []
            }],
        "parent_hcos__v": [],
        "training_set__c": []
    }
}               

Troubleshooting widgets

In rare circumstances, you may need to embed your widget in an iframe to access it properly.

This may be the case if, during widget loading, an exception indicates that resources are already in use or that elements have been duplicated on the page.

This results in a blank space appearing where the widget should be.

Creating the iframe document

The details of the widget are contained in a separate file; for example, widget_embed.html.

The embed file contains the usual definition for the widget, as follows:

widget_embed.html

<veeva-network-search-widget
    id="searchWidget"
    widget-name="Expense"
    auth-domain=“verteo.vdmdev.com"
    widget-id="NTE0Ozs7dGVzdGluZ19fYw==">
</veeva-network-search-widget>

In addition to the widget definition, this file also contains the requisite handler and listener for events within the embedded widget.

In this example, the event data to be passed is defined in the eventData declaration.

<!-- Embedded widget event handling -->
<script type="text/javascript">
    // this method handles the generic behaviour of an emitted Network widget
    function handleWidgetEvent(ev) {
        var iframeURL = 'http://localhost:8000';

        // only pass this subset of the event details
        var eventData = {
            type: ev.type,
            detail: ev.detail
        };

        // the event data must be represented as a string
        eventData = JSON.stringify(eventData);

        // post the message to the parent that hosts this iframe
        window.parent.postMessage(eventData, iframeURL);
    }

    // add usual event listeners for the embedded widget
    window.addEventListener('veeva-network:Expense:select', handleWidgetEvent);
</script>

The event data, along with the URL of the embed, is passed to the parent document (in this case, the index.html file that embeds it), through the window.parent.postMessage method.

Embedding the widget file

The widget file you create is referenced from the application’s index.html file:

index.html

<!-- Sample iframe definition →
<div id="iframeContainer">
    <iframe id="widgetiframe" src=“widget_embed.html"></iframe>
</div>

This file also contains the hooks required to receive event data, through the window.addEventListener call.

<!-- Embedded widget event handling -->
<script type="text/javascript">
    // listen to any events emitted by the iframe
    window.addEventListener('message', function(ev) {
        var iframeURL = 'http://localhost:8000';

        // disallow any messages incoming from another origin
        if (ev.origin !== iframeURL) return;

        // we need to parse the event data that was stringified
            in the iframe
        var eventData = JSON.parse(ev.data);

        // handle embedded widget events here
        switch (eventData.type) {
            case 'veeva-network:Expense:select':
            console.log('Received', eventData.type, 'with data:',           eventData.detail, 'outside iframe');
            logEventData(eventData);
            break;
            default:
            break;
        }
    });

    function logEventData(eventData) {
        var eventLogEl = document.getElementById('eventLog');
        eventLogEl.innerText = eventLogEl.innerText + 
            JSON.stringify(eventData) + '\n';
    }
</script>

The message attribute in the call is derived from the event data posted from the windowparent.postMessage call in the widget_embed.html file.