Saturday, January 20, 2018

microsoft dynamics crm and electron web app.md

microsoft dynamics crm and electron web app.md

You can easily create a desktop app using javascript, css and html. The electron platform app allows you to create a javascript app, just like you would with the web version of dynamics crm and place it on the desktop. The only real difference is that you are not using crm forms. It’s a custom app.

Here’s a recipe for doing it:

  • create your web app. In my case, I have a “tool” that I use that typically loads in as a dynamics solution. I’m going to modify only a few lines to get it to work with electron. The tool shows plugin trace logs and updates itself using polling to track the latest log.
  • node
  • electron
  • react
  • office-ui-fabric-react

Modify Your App Entry Point

I use the dynamics-client-ui client ui toolkit for creating react based interfaces for dynamics crm. The other code has been touched to include some electron specific init code:

import * as React from "react"
import * as ReactDOM from "react-dom"
import * as PropTypes from "prop-types"
const cx = require("classnames")
const fstyles = require("dynamics-client-ui/lib/Dynamics/flexutilities.css")
const styles = require("./App.css")
import { Fabric } from "office-ui-fabric-react/lib/Fabric"
import { PluginTraceLogViewer } from "./PluginTraceLogViewer"
import { Navigation } from "./Navigation"
import { Dynamics, DynamicsContext } from "dynamics-client-ui/lib/Dynamics/Dynamics"
import { getXrm, getGlobalContext, isElectron } from "dynamics-client-ui/lib/Dynamics/Utils"
import { XRM, Client, mkClientForURL } from "dynamics-client-ui"
import { BUILD, DEBUG, API_POSTFIX, EXEC_ENV } from "BuildSettings"
import "dynamics-client-ui/lib/fabric/ensureIcons"
import { Config, fromConfig } from "dynamics-client-ui/lib/Data"

let config: Config

if (EXEC_ENV === "ELECTRON") {
    console.log("Configuring data access for electron")
    const electron = require("electron")
    const tokenResponse = electron.remote.getGlobal("adalToken")
    const adalConfig = electron.remote.getGlobal("adalConfig")
    if (!tokenResponse || !adalConfig) console.log("Main vars were not passed through correctly. tokenResponse:", tokenResponse, "adalConfig:", adalConfig)
    config = { APIUrl: adalConfig.dataUrl, AccessToken: () => tokenResponse.accessToken }
} else {
    console.log("Configuring data access assuming in-server web")
    config = { APIUrl: getGlobalContext().getClientUrl() }
}

export interface Props {
    className?: string
    config: Config
}

export class App extends React.Component<Props, any> {
    constructor(props, context) {
        super(props, context)
        this.client = fromConfig(props.config)
    }
    private client: Client

    public static contextTypes = {
        ...Dynamics.childContextTypes,
    }

    public render() {
        return (
            <div
                data-ctag="App"
                className={cx(fstyles.flexHorizontal, styles.app, this.props.className)}
            >
                {false && <Navigation className={cx(fstyles.flexNone, styles.nav)} />}
                <PluginTraceLogViewer
                    client={this.client}
                    className={cx(styles.plugin, fstyles.flexAuto)}
                />
            </div>
        )
    }
}

export function run(el: HTMLElement) {
    ReactDOM.render(
        <Fabric>
            <App
                className={cx(styles.topLevel)}
                config={config}
            />
        </Fabric>,
        el)
}

// shim support
if ((BUILD !== "PROD" && typeof runmain !== "undefined" && runmain === true) ||
    EXEC_ENV === "ELECTRON") {
    window.addEventListener("load", () => {
        // @ts-ignore
        run(document.getElementById("container"))
    })
}

You can see that very little code has been touched to adapt to some javascript injected from the main electron process.

My electron start up code is:

const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')
const fs = require("fs")
const adal = require("adal-node")
const AuthenticationContext = adal.AuthenticationContext

function turnOnLogging() {
    var log = adal.Logging
    log.setLoggingOptions(
        {
            level: log.LOGGING_LEVEL.VERBOSE,
            log: function (level, message, error) {
                console.log(message)
                if (error) {
                    console.log(error)
                }
            }
        })
}
//turnOnLogging()

const argsCmd = process.argv.slice(2);
console.log("ADAL configuration file", argsCmd[0])
const adalConfig = JSON.parse(fs.readFileSync(argsCmd[0]))
global.adalConfig = adalConfig
const authorityHostUrl = adalConfig.authorityHostUrl + "/" + adalConfig.tenant
const context = new AuthenticationContext(authorityHostUrl)
let adalToken = new Promise((res, rej) => {
    context.acquireTokenWithUsernamePassword(adalConfig.acquireTokenResource,
        adalConfig.username,
        adalConfig.password || process.env["DYNAMICS_PASSWORD"],
        adalConfig.applicationId,
        (err, tokenResponse) => {
            if (err) {
                console.log(Error, err)
                global.adalToken = null
                adalToken = null
                rej(err)
            } else {
                console.log("ADAL token response:", tokenResponse)
                adalToken = tokenResponse
                global.adalToken = tokenResponse
                res(adalToken)
            }
        })
})

// refresh with
//context.acquireTokenWithRefreshToken(tokenResponse['refreshToken'], adalConfig.clientId, null, (e, t) => {...})


// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow() {
    return adalToken.then(tok => {
        // Create the browser window.
        win = new BrowserWindow({ width: 800, height: 600 })

        // and load the index.html of the app.
        win.loadURL(url.format({
            pathname: path.join(__dirname, "dist", "ttg_", "WebUtilities", "App.electron.html"),
            protocol: 'file:',
            slashes: true
        }))

        // Open the DevTools.
        win.webContents.openDevTools()

        // Emitted when the window is closed.
        win.on('closed', () => {
            // Dereference the window object, usually you would store windows
            // in an array if your app supports multi windows, this is the time
            // when you should delete the corresponding element.
            win = null
        })
    })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow()
    }
})

I won’t show all the gooey webpack config code, but the key is to ensure that your target is set to “electron” so that various node_module/electron* lurking modules are found correctly.

The only key thing about this code, which is almost exactly the code found on the electron website with a small tweak for adal authentication, is that very little needs to be done to do something quickly. Obviously, much more effort is needed to make this more usable e.g. configure some menus.

Note the promise setup on the token. If the authentication takes too long compared to the startup of the embedded web browser, you will have a sequencing issue. We use the promise effect to sequence the startup.

I can run this from my dev directory using npx electron . <path to crm adal .json config file>.

I’ll post the entire project to github at some point.