Below is the sample code to create a SPA to perform CRUD operation on D365 using WebAPI and ADAL for authentication. The code demonstrate use of GET, POST, PATCH and DELETE for Retrieve, Create, Update and Delete respectively. The sample code provided from Microsoft is used here as base and additional feature are added on top of it.
<!DOCTYPE html>
<html>
<head>
<title>D365 Single Page Application for D365 using jqGrid</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/css/ui.jqgrid.min.css">
<a href="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js">https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js</a>
<a href="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js">https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js</a>
<a href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js">https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js</a>
<script type="text/javascript">
"use strict";
var organizationURI = "https://xxxx.crm.dynamics.com"; //The URL of your Dataverse organization
var tenant = "xxxx.onmicrosoft.com"; //The name of the Azure AD organization you use
var clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; //The ClientId you got when you registered the application
var pageUrl = "https://localhost:44347/SHtmlPage.html"; //The URL of this page in your development environment when debugging.
var user, authContext, message, errorMessage, loginButton, logoutButton, getAccountsButton;
//Configuration data for AuthenticationContext
var endpoints = {
orgUri: organizationURI
};
window.config = {
tenant: tenant,
clientId: clientId,
postLogoutRedirectUri: pageUrl,
endpoints: endpoints,
cacheLocation: 'localStorage',
};
document.onreadystatechange = function () {
if (document.readyState == "complete") {
//Set DOM elements referenced by scripts
message = document.getElementById("message");
errorMessage = document.getElementById("errorMessage");
loginButton = document.getElementById("login");
logoutButton = document.getElementById("logout");
getAccountsButton = document.getElementById("getAccounts");
//Event handlers on DOM elements
loginButton.addEventListener("click", login);
logoutButton.addEventListener("click", logout);
getAccountsButton.addEventListener("click", getAccounts);
//call authentication function
authenticate();
if (user) {
loginButton.style.display = "none";
logoutButton.style.display = "block";
getAccountsButton.style.display = "block";
var helloMessage = document.createElement("p");
helloMessage.textContent = "Hello " + user.profile.name;
message.appendChild(helloMessage)
}
else {
loginButton.style.display = "block";
logoutButton.style.display = "none";
getAccountsButton.style.display = "none";
}
}
}
// Function that manages authentication
function authenticate() {
//OAuth context
authContext = new AuthenticationContext(config);
// Check For & Handle Redirect From AAD After Login
var isCallback = authContext.isCallback(window.location.hash);
if (isCallback) {
authContext.handleWindowCallback();
}
var loginError = authContext.getLoginError();
if (isCallback && !loginError) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
}
else {
errorMessage.textContent = loginError;
}
user = authContext.getCachedUser();
}
//function that logs in the user
function login() {
authContext.login();
}
//function that logs out the user
function logout() {
authContext.logOut();
}
//function that initiates retrieval of accounts
function getAccounts() {
getAccountsButton.disabled = true;
var retrievingAccountsMessage = document.createElement("p");
retrievingAccountsMessage.textContent = "Retrieving 10 accounts from " + organizationURI + "/api/data/v9.1/accounts";
message.appendChild(retrievingAccountsMessage)
// Function to perform operation is passed as a parameter to the acquireToken method
authContext.acquireToken(organizationURI, retrieveAccounts)
}
//Function that actually retrieves the accounts
function renderAccounts(accounts, token) {
var listOfAccounts = [];
accounts.forEach(function (account) {
var name = account.name;
var city = account.address1_city;
var accountid = account.accountid;
listOfAccounts.push({
name: name,
city: city,
accountid: accountid
});
});
$("#grid").jqGrid({
colModel: [
{ name: "name", width: 300, editable: true},
{ name: "city", width: 300, editable: true },
{ name: "accountid", width: 300, editable: true, editrules: { edithidden: false }, hidden: true },
],
pager: '#pager2',
sortname: 'name',
viewrecords: true,
sortorder: "desc",
caption: "",
editable: true,
data: listOfAccounts
});
$("#grid").jqGrid('navGrid', '#pager2',
{
edit: true,
add: true,
del: true,
search: false,
refresh: false,
view: false
},
{
editCaption: "Edit Account",
edittext: "Edit",
closeOnEscape: true,
closeAfterEdit: true,
savekey: [true, 13],
errorTextFormat: commonError,
reloadAfterSubmit: true,
onclickSubmit: function (response, postdata) {
if (confirm('Are you sure you want to update this row?')) {
UpdateAccount(postdata, token);
return [true, ''];
} else {
return [false, 'You can not submit!'];
}
}
},
//add Options. save key parameter will keybind the Enter key to submit.
{
addCaption: "Add Account",
addtext: "Add Account",
closeAfterAdd: true,
recreateForm: true,
errorTextFormat: commonError,
onclickSubmit: function (response, postdata) {
postdata.accountid = AddAccount(postdata, token);
}
},
{
deleteCaption: "delete Account",
deletetext: "Delete Account",
closeOnEscape: true,
closeAfterEdit: true,
savekey: [true, 13],
errorTextFormat: commonError,
reloadAfterSubmit: true,
onclickSubmit: function (response, postdata) {
var sr = jQuery("#grid").getGridParam('selrow');
var rowdata = jQuery("#grid").getRowData(sr);
DeleteAccount(rowdata, token);
}
},
{},
);
}
function retrieveAccounts(error, token) {
// Handle ADAL Errors.
if (error || !token) {
errorMessage.textContent = 'ADAL error occurred: ' + error;
return;
}
var req = new XMLHttpRequest()
req.open("GET", encodeURI(organizationURI + "/api/data/v9.1/accounts?$select=name,address1_city,accountid&$top=10"), true);
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (this.readyState == 4 /* complete */) {
req.onreadystatechange = null;
if (this.status == 200) {
var accounts = JSON.parse(this.response).value;
renderAccounts(accounts, token);
}
else {
var error = JSON.parse(this.response).error;
console.log(error.message);
errorMessage.textContent = error.message;
}
}
};
req.send();
}
function UpdateAccount(params, token) {
// Handle ADAL Errors.
if (error || !token) {
errorMessage.textContent = 'ADAL error occurred: ' + error;
return;
}
var req = new XMLHttpRequest();
req.open("PATCH", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
var body = JSON.stringify({
"name": params.name,
"address1_city": params.city
});
req.send(body);
if (req.readyState === 4) {
if (req.status === 204) {
var uri = req.getResponseHeader("OData-EntityId");
var regExp = /\(([^)]+)\)/;
var matches = regExp.exec(uri);
var accountid = matches[1];
alert(params.name + " Updated");
}
else {
var error = JSON.parse(req.response).error;
alert(error.message);
}
}
}
function AddAccount(params, token) {
// Handle ADAL Errors.
if (error || !token) {
errorMessage.textContent = 'ADAL error occurred: ' + error;
return;
}
var account = {};
account["name"] = params.name;
account["address1_city"] = params.city;
var req = new XMLHttpRequest();
req.open("POST", encodeURI(organizationURI + "/api/data/v9.1/accounts"), false);
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.send(JSON.stringify(account));
if (req.readyState === 4) {
if (req.status === 204) {
var uri = req.getResponseHeader("OData-EntityId");
var regExp = /\(([^)]+)\)/;
var matches = regExp.exec(uri);
var accountid = matches[1];
alert(params.name + " Created");
return accountid;
}
else {
var error = JSON.parse(req.response).error;
alert(error.message);
}
}
}
function DeleteAccount(params, token) {
// Handle ADAL Errors.
if (error || !token) {
errorMessage.textContent = 'ADAL error occurred: ' + error;
return;
}
var req = new XMLHttpRequest();
req.open("DELETE", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.send();
if (req.readyState === 4) {
if (req.status === 204) {
alert(params.name + " Deleted");
}
else {
var error = JSON.parse(req.response).error;
alert(error.message);
}
}
}
function commonError(data) {
return "Error Occured during Operation. Please try again";
}
</script>
<style>
body {
font-family: 'Segoe UI';
}
table {
border-collapse: collapse;
}
td, th {
border: 1px solid black;
}
#errorMessage {
color: red;
}
#message {
color: green;
}
</style>
</head>
<body>
<button id="login">Login</button>
<button id="logout" style="display:none;">Logout</button>
<button id="getAccounts" style="display:none;">Get Accounts</button>
<div id="errorMessage"></div>
<div id="message"></div>
<table id="grid"></table>
<div id="pager2"></div>
</body>
</html>