Single Page Application(SPA) – CRUD operation in D365 using jqGrid

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>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: