const R = require("ramda") export * from "./getXrmP" import { XRM } from "./xrm" import { DEBUG} from "BuildSettings" /** * Get a URL parameter from `search` or `document.loction.search` by * default. This is useful for obtaining the "data" parameter from the URL * that is passed in from a form if you set the WebResource url properties. * Sometimes the API does not seem to work, but this seems to always work. */ export function getURLParameter(name: string, search: string = document.location.search): string | null { search = search || "" const r: Array | null = new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(search) const r2: Array = ["", ""] return decodeURIComponent((r || r2)[1].replace(/\+/g, "%20")) || null; } /** Generate a unique id with an optional prefix. */ export function generateId(prefix: string = "") { return `${prefix}-${uuidv4()}` } const dec2hex: string[] = []; for (let i = 0; i <= 15; i++) { dec2hex[i] = i.toString(16); } const UUID = () => { let uuid = "" for (let i = 1; i <= 36; i++) { if (i === 9 || i === 14 || i === 19 || i === 24) { uuid += "-" } else if (i === 15) { uuid += 4 } else if (i === 20) { uuid += dec2hex[(Math.random() * 4 | 0 + 8)] } else { uuid += dec2hex[(Math.random() * 15 | 0)] } } return uuid; } /** Probably need something multi-browser friendly here. */ //export function uuidv4(): string { // return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => // (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) // ) //} export function uuidv4(): string { return UUID() } /** Internal to this module. ... */ function cleanId(id: string): string { if (typeof id === "undefined" || id === null) throw Error(`Unable to clean nil id ${id}`) return id.toString().replace(/[{}]/g, "").toLowerCase() } /** Uses internal API. */ export function isUci(xrm: XRM): boolean { if (xrm.Internal && xrm.Internal.isUci) return xrm.Internal.isUci() return false } let _isElectron: boolean = false const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.indexOf(" electron/") > -1) { _isElectron = true } /** Return true if we are running inside electron. */ export function isElectron(): boolean { return _isElectron } /** * Checks the form context to see if there is an entity id. If not, * it's probably a new form. */ export function hasEntityId(xrm: XRM | null): boolean { if (!xrm) return false const id = entityIdOrNull(xrm) if (id) return true return false } /** Return the entity id on the page context or null. Braces removed. */ export function entityIdOrNull(xrm: XRM | null): string | null { if (!xrm) return null const e = eaccess(xrm) if (e) return cleanId(e.getId()) return null } const eaccess = R.pathOr(null, ["Page", "data", "entity"]) /** * After a save event, run actionToTake if ready returns ture. Uses polling. * Once actionToTake is run, the polling is removed. An exception thrown in * in ready is considered a false return. * @param xrm Xrm to attach to Xrm.Page.data.entity.addOnSave/removeOnSave * @param ready Return true if the condition to run actionToTake has been met. * @param actionToTake The action to take. * @param pollInterval The interval to poll that ready is true after the save has occurred. * @return A cancellable thunk. */ export function runAfterSave(xrm: XRM, ready: (x?: XRM) => boolean | null | undefined, actionToTake: (x?: XRM) => void, pollInterval: number = 500): () => void { let cancellable: any | null = null const onSaveHandler = (ctx) => { const keepChecking = () => { try { const fire = ready(xrm) if (fire) { xrm.Page.data.entity.removeOnSave(onSaveHandler) clearInterval(cancellable) actionToTake(xrm) } } catch (e) { // do nothing console.log("Ready check failed", e) } } cancellable = setInterval(keepChecking, pollInterval) } xrm.Page.data.entity.addOnSave(onSaveHandler) return () => xrm.Page.data.entity.removeOnSave(onSaveHandler) } /** Render a null, which in react means no rendering. */ export const RenderNothing = () => null /** Do nothing. */ export function noop() { } /** * If cb is a function, return it, otherwise noop. * @param cb Callback */ export function callbackOrNoop(cb) { return typeof cb === "function" ? cb : noop } /** * If arg is an array, return the first element if it exists, * otherwise, return other. */ export function firstOrElse(arg, other) { arg = Array.isArray(arg) ? arg[0] : arg if (R.isNil(arg) && other) return other; else return arg; } /** Find the first undefined array element or return undefined. */ export function firstUndefined(...args) { if (!Array.isArray(args)) return undefined return args.find(a => typeof a !== "undefined") } /** * Execute fns with the same args in order until * `event.preventDefault()` is called. This is really * just a takeWhile and map where "event" is mutable state. */ export function composeEventHandlers(...fns) { return (event, ...args) => { fns.some(fn => { // does this test in array order??? fn && fn(event, ...args) return event.defaultPrevented }) } } /** Per downshift, (p)react. */ export function isDOMElement(el) { if (el) { if (el.nodeName) return typeof el.nodeName === "string" else return (typeof el.type === "string" || typeof el.type === "function") } return false } /** Return true if its a number. */ export function isNumber(thing) { // eslint-disable-next-line no-self-compare return thing === thing && typeof thing === "number" } /** * Get props for (p)react. */ export function getElementProps(element) { return element.props || element.attributes } /** * Return the parent's Xrm from window.parent.Xrm or window.Xrm. * No check to see if Xrm.Page.data is present as that is deprecated. * This is a strict value check, no async. * * @see getXrmForEntity */ export function getXrm(): XRM | null { return (window.parent.Xrm as XRM) || (window.Xrm as XRM) } /** * Return the global context from GetGlobalContext(), then * Xrm.Utility.getGlobalContext() then Xrm.Page.context. * Throws Error if not found. * * @see https://msdn.microsoft.com/pt-pt/library/af74d417-1359-4eaa-9f87-5b33a8852e83(v=crm.7) */ export function getGlobalContext(): Xrm.GlobalContext { var errorMessage = "Context is not available."; if (typeof GetGlobalContext !== "undefined") { return GetGlobalContext() } else { if (typeof Xrm !== "undefined") { if(typeof Xrm.Utility !== "undefined" && typeof Xrm.Utility.getGlobalContext !== "undefined") { return Xrm.Utility.getGlobalContext() } // Try this... return Xrm.Page.context } else { throw new Error(errorMessage) } } } /** * Walk the window chain looking for Xrm with Xrm.Page.data attribute being * non-null. Return null if not found. It will walk the window hierarchy * as well as test some well known locations of Xrm. * * @see getXrm */ export function getXrmForEntity(): XRM | null { const window: Window | null = walkParents({ select: (w: Window) => R.pathOr(false, ["Xrm", "Page", "data"], w) }) if (window) return window.Xrm as XRM const maybeXrm = getXrm() if (R.pathOr(false, ["Page", "data"], maybeXrm)) return maybeXrm return null } /** * Run a thunk up the window chain. Return the last window visited if it meets * select criteria (if provided) or if select returns true for a particular * window. Return null otherwise. Thunk is usually used for logging. */ export function walkParents({ thunk, select, max }: { thunk?: (w: Window) => void select: (w: Window) => boolean max?: number }): Window | null { max = max || 10 let current: Window | null = window // this window while (current && max > 0 && !select(current)) { if (thunk) thunk(current) max = max - 1 if (current.parent === current) current = null else current = current.parent } if (select(current!)) return current return null } /** * Try to get entityid, userid, entity name, entity type code (number) from a * variety of places including the Xrm values and the URL parameters in the * document that the function is called from. Varibles that are found are * returned but if something is not found it is not returned. If its a new * entity, obviously, the entityid will not be present. Return an object with * {userId, entityId, entityName, entityTypeCode}. Note that entityTypeCode * is specific to an organization so do not use it if you can avoid it. * * Should typecode be number or string? */ export function getEntityInfo(xrm?: XRM | null): { userId?: string entityId?: string entityName?: string entityTypeCode?: number } { const x = xrm || getXrmForEntity() const context = R.pathOr(null, ["Page", "context"], x) const entity = R.pathOr(null, ["Page", "data", "entity"], x) const etn = getURLParameter("etn") const typename = getURLParameter("typename") const etc = context ? parseInt(context.getQueryStringParameters().etc) : null const eid = (entity && entity.getId()) || getURLParameter("id") || null const uid = (context && context.getUserId()) || null const ename = entity ? entity.getEntityName() : (etn ? etn : (typename ? typename : null)) const tcode: number | null = etc const rval = { ...(uid ? { userId: cleanId(uid) } : {}), ...(eid ? { entityId: cleanId(eid) } : {}), ...(ename ? { entityName: ename } : {}), ...(tcode ? { entityTypeCode: tcode } : {}) } return rval } /** * Access page context to return form type. * @deprecated Use XRM members directly. */ export function getFormType(xrm: XRM): XrmEnum.FormType { const v = xrm.Page.ui.getFormType() // check range??? return v as XrmEnum.FormType } /** If create form, checks formtype and whether there is an id. */ export function isCreateForm(xrm: XRM): boolean { return getFormType(xrm) === XrmEnum.FormType.Create || !eaccess(xrm).getId() } /** * Load scripts programmatically. The script is evaluated once loaded by the browser/host. */ export function loadScripts(scripts: Array, callback: () => void, targetDoc: Document = document) { const loader = (src: string, handler: () => void) => { if(DEBUG) console.log("Programmatically loading: " + src) const script = targetDoc.createElement("script") script.src = src script.onload = () => { // remove onload handler?? handler() } const head = targetDoc.getElementsByTagName("head")[0]; (head || targetDoc.body).appendChild(script) } // Run on each script... (function run() { if (scripts.length > 0) { loader(scripts.shift()!, run) } else if (callback) callback() })() }