CRUD operation on Single Page Application for D365 using MSAL with jqGrid

We are re-creating our SPA but using MSAL instead of ADAL. MSAL allow authentication using wider set of identities instead of just Azure AD as for ADAL. The list of differences can be found here.

Below is the sample code for performing CRUD operation in jqGrid for D365 using MSAL.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <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://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.js">https://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.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">
        const msalConfig = {
            auth: {
                clientId: "xxxxxxxxx-f8d4-448b-ba96-e3097cd7c3dd",
                authority: "https://login.microsoftonline.com/common",
                redirectUri: "https://localhost:44308/SinglePageAppMSAL.html"
            },
            cache: {
                cacheLocation: "sessionStorage", // This configures where your cache will be stored
                storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
            },
            system: {
                loggerOptions: {
                    loggerCallback: (level, message, containsPii) => {
                        if (containsPii) {
                            return;
                        }
                        switch (level) {
                            case msal.LogLevel.Error:
                                console.error(message);
                                return;
                            case msal.LogLevel.Info:
                                console.info(message);
                                return;
                            case msal.LogLevel.Verbose:
                                console.debug(message);
                                return;
                            case msal.LogLevel.Warning:
                                console.warn(message);
                                return;
                        }
                    }
                }
            }
        };

        const loginRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"]
        };


        const tokenRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"],
            forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
        };


        const myMSALObj = new msal.PublicClientApplication(msalConfig);
        let accessToken;
        let username = "";

        myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
            console.error(err);
        });

        function handleResponse(resp) {
            if (resp !== null) {
                username = resp.account.username;
                loginButton.style.display = "none";
                logoutButton.style.display = "block";
                getAccountsButton.style.display = "block";

                var helloMessage = document.createElement("p");
                helloMessage.textContent = "Hello " + username;
                message.appendChild(helloMessage);
            }
            else {
                const currentAccounts = myMSALObj.getAllAccounts();
                if (currentAccounts === null) {

                    return;
                } else if (currentAccounts.length > 1) {
                    // Add choose account code here
                    console.warn("Multiple accounts detected.");
                } else if (currentAccounts.length === 1) {
                    username = currentAccounts[0].username;
                }
            }
        }

        function signIn() {
            myMSALObj.loginRedirect(loginRequest);            
        }

        function signOut() {
            const logoutRequest = {
                account: myMSALObj.getAccountByUsername(username)
            };
            myMSALObj.logout(logoutRequest);
        }

        function getTokenRedirect(request) {
            request.account = myMSALObj.getAccountByUsername(username);
            return myMSALObj.acquireTokenSilent(request).catch(error => {
                console.warn("silent token acquisition fails. acquiring token using redirect");
                if (error instanceof msal.InteractionRequiredAuthError) {
                    return myMSALObj.acquireTokenRedirect(request);
                } else {
                    console.warn(error);
                }
            });
        }

        function readAccount() {
            getTokenRedirect(tokenRequest).then(response => {
                retrieveAccounts(response.accessToken);
            }).catch(error => {
                console.error(error);
            });
        }

        function renderAccount(accounts, token) {
            $("#grid").clearGridData();
            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").setGridParam({ data: listOfAccounts }).trigger("reloadGrid");
            $("#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!'];
                        }
                    }
                },
                {
                    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(token) {
            if (!token) {
                errorMessage.textContent = 'error occurred: ';
                return;
            }
            
            var req = new XMLHttpRequest()
            req.open("GET", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/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 account = JSON.parse(this.response).value;
                        renderAccount(account, token);
                    }
                    else {
                        var error = JSON.parse(this.response).error;
                        console.log(error.message);
                        errorMessage.textContent = error.message;
                    }
                }
            };
            req.send();
        }


        function UpdateAccount(params, token) {
            var req = new XMLHttpRequest();
            req.open("PATCH", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/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) {
            var account = {};
            account["name"] = params.name;
            account["address1_city"] = params.city;
            var req = new XMLHttpRequest();
            req.open("POST", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/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) {
            var req = new XMLHttpRequest();
            req.open("DELETE", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/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";
        }
        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", signIn);
                logoutButton.addEventListener("click", signOut);
                getAccountsButton.addEventListener("click", readAccount);

                if (username) {
                    loginButton.style.display = "none";
                    logoutButton.style.display = "block";
                    getAccountsButton.style.display = "block";

                    var helloMessage = document.createElement("p");
                    helloMessage.textContent = "Hello " + username;
                    message.appendChild(helloMessage)

                }
                else {
                    loginButton.style.display = "block";
                    logoutButton.style.display = "none";
                    getAccountsButton.style.display = "none";
                }
            }
        }
    </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: