/** * Created by rburson on 1/6/16. */ import * as React from 'react' import { NavRequest, AppWinDef, AppContext, FormContext, Log, PaneContext, StringUtil, EntityRec, EntityBuffer, NullEntityRec, EntityRecImpl, WebRedirection, WorkbenchLaunchAction, PropDef, DialogRedirection, DialogHandle, SessionContext, SessionContextImpl, SystemContext, SystemContextImpl, Future, ActionSource, ContextAction } from 'catavolt-sdk' /* ****************************************************************** * Base Interfaces/Objects ****************************************************************** */ /** Base interface for Catavolt component properties Components may choose to support any of these properties */ export interface CvProps { /** * The Catavolt SDK entry point, an instance of the sdk {AppContext} which is always available to components. * A singleton instance is used by the underlying components to interact with the sdk. */ catavolt?:AppContext; /** * The {@link CvEventRegistry} handles decoupled communication between components. Any component may subscribe to and publish * {@link CvEvent}s. See also {@link CvListener} and {@link CvEventType} */ eventRegistry?:CvEventRegistry; /** * Allows for a component's rendering logic to be overridden and completely customized. * The first argument, the {@link CvContext}, exposes the {@link CvScopeContext} which provides the relevant SDK object * for use by the renderer. {@link CvScopeContext.scopeObj} will always be an SDK object exposing the data related to * the current component. */ renderer?:(cvContext:CvContext, callbackObj?:any)=>{} } /** Base interface for catavolt component state */ export interface CvState { } /** Top-level Catavolt Context Object */ export interface CvContext { /** * The Catavolt SDK entry point, an instance of the sdk {AppContext} which is always available to components. * A singleton instance is used by the underlying components to interact with the sdk. */ catavolt?:AppContext; /** * The {@link CvEventRegistry} handles decoupled communication between components. Any component may subscribe to and publish * {@link CvEvent}s. See also {@link CvListener} and {@link CvEventType} */ eventRegistry?:CvEventRegistry; /** Allows for exposing a 'scope' object to child components This is usually the corresponding SDK object */ scopeCtx?:CvScopeContext; } /** Allows for exposing a 'scope' object to child components This is usually the corresponding SDK object */ export interface CvScopeContext { /** * The object relevant to this components 'scope'. Usually an SDK object corresponding to the component type. */ scopeObj:any; /** * The parent's scope context (if any) * This allows for navigation back up the component hierarchy */ parentScopeCtx:CvScopeContext; } /** Base Mixin for all catavolt components */ export var CvBaseMixin = { contextTypes: { cvContext: React.PropTypes.object }, childContextTypes: { cvContext: React.PropTypes.object }, catavolt: function ():AppContext { return this.props.catavolt || (this.context.cvContext && this.context.cvContext.catavolt); }, eventRegistry: function ():CvEventRegistry { return this.props.eventRegistry || (this.context.cvContext && this.context.cvContext.eventRegistry); }, findFirstDescendant: function (elem, filter:(o)=>boolean) { var result = null; if (elem.props && elem.props.children) { var elems:Array = React.Children.toArray(elem.props.children); for (let i = 0; i < elems.length; i++) { const child = elems[i]; console.log(child); if (filter(child)) { result = child; } else if (child.props.children) { result = this.findFirstDescendant(child, filter); } } } return result ? result : null; }, findAllDescendants: function (elem, filter:(o)=>boolean, results:Array = []):Array { if (elem.props && elem.props.children) { var elems:Array = React.Children.toArray(elem.props.children); for (let i = 0; i < elems.length; i++) { const child = elems[i]; console.log(child); if (filter(child)) { results.push(child); } if (child.props && child.props.children) { this.findAllDescendants(child, filter, results); } } } return results; }, findFirstScopeCtx: function (matchFn:(scopeCtx:CvScopeContext)=>boolean, startingCtx?:CvScopeContext):CvScopeContext { let scopeCtx = startingCtx || this.scopeCtx(); let result:CvScopeContext = null; while (scopeCtx && !result) { if (matchFn(scopeCtx)) { result = scopeCtx; } else { scopeCtx = scopeCtx.parentScopeCtx; } } return result; }, findEntityRec: function (startingContext?:CvScopeContext) { if (this.props.entityRec) { return this.props.entityRec; } else { var e:EntityRec = null; const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> { return scopeCtx.scopeObj instanceof EntityBuffer || scopeCtx.scopeObj instanceof EntityRecImpl || scopeCtx.scopeObj instanceof NullEntityRec; }, startingContext) return scopeCtx ? scopeCtx.scopeObj : null; } }, findPaneContext: function (startingContext:CvScopeContext) { if (this.props.paneContext) { return this.props.paneContext; } else { const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> { return scopeCtx.scopeObj instanceof PaneContext; }, startingContext) return scopeCtx ? (scopeCtx.scopeObj as PaneContext) : null; } }, findPaneTitle: function (paneContext:PaneContext) { let title = paneContext.paneTitle; if (!title) title = paneContext.actionSource instanceof WorkbenchLaunchAction ? (paneContext.actionSource as WorkbenchLaunchAction).name : null; if (!title) title = paneContext.paneDef.settings['dialogDescription']; if (!title || title === 'null') { return ''; } else { return title; } }, firstInScope: function (type, startingScopeCtx?:CvScopeContext) { const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> { return scopeCtx.scopeObj instanceof type; }, startingScopeCtx) return scopeCtx ? scopeCtx.scopeObj : null; }, getDefaultChildContext: function () { return { cvContext: { catavolt: this.catavolt(), eventRegistry: this.eventRegistry(), scopeCtx: { scopeObj: null, parentScopeCtx: this.scopeCtx() } } } }, scopeCtx: function ():CvScopeContext { return this.context.cvContext && this.context.cvContext.scopeCtx; } } /** ****************************************************************** * General Callback mechanism ****************************************************************** */ export interface CvResultCallback {(success:A, error?:any):void } /* ****************************************************************** Component Event Registry Framework for decoupled communication between our components ****************************************************************** */ export interface CvListener { (event:CvEvent):void; } export interface CvEvent { type:CvEventType; eventObj:T; resourceId?:string; } /** * Enumeration of event types to be used with {@link CvEvent} * There is also a corresponding 'payload' object type for each of these * enum values, that is used as the {@link CvEvent.eventObj} type */ export enum CvEventType { LOGIN, LOGOUT, ACTION_FIRED, NAVIGATION, STATE_CHANGE, MESSAGE, SESSION_UPDATE } /* Event type payloads */ export interface CvLoginResult { appWinDef:AppWinDef } export interface CvLogoutResult { tenantId:string; } export enum CvNavigationResultType { FORM, URL, NULL } export interface CvNavigationResult { navRequest:NavRequest; workbenchId?:string; actionId?:string; navTarget?:string; sourceIsDestroyed?:boolean; noTransition?:boolean; type:CvNavigationResultType; } export enum CvMessageType { ERROR, WARN, INFO } export interface CvMessage { type:CvMessageType; message:string; messageObj?:any; } export interface CvSessionUpdateResult { appWinDef:AppWinDef } export enum CvStateChangeType { PANE_DEF_CHANGE, DATA_CHANGE, DESTROYED, MODE_CHANGE_READ, MODE_CHANGE_WRITE } export interface CvStateChangeResult { source?:PaneContext; type:CvStateChangeType; } export enum CvActionFiredResultType { ACTION_STARTED, ACTION_COMPLETED } export interface CvActionFiredResult { actionId:string; type:CvActionFiredResultType; source:PaneContext; clientAction:boolean; } /** * The {@link CvEventRegistry} handles decoupled communication between components. Any component may subscribe to and publish * {@link CvEvent}s. See also {@link CvListener} and {@link CvEventType}. It also provides resource caching (i.e. caching of * some server provided objects to allow retrieval by 'id') */ export class CvEventRegistry { private _cache:{[index:string]:CvEvent} = null; private _listenerMap:{[index:number]:Array>} = []; constructor() { } clearAll():void { this.clearCache(); this.removeAllListeners(); } clearCache():void{ this._cache = {} } enableCache():void { this._cache = this._cache || {}; } disableCache():void { this._cache = null; } isCacheEnabled():boolean { return this._cache !== null; } getEventByKey(key:string):CvEvent { const result = this.isCacheEnabled() ? this._cache[key] : null; if (!result) { Log.debug('No resource entry exists for resourceId ' + key); } return result; } publish(event:CvEvent, shouldCache:boolean = false):void { const listenerArray:Array> = this._listenerMap[event.type]; if (listenerArray) { listenerArray.forEach((listener:CvListener)=> { Log.debug('publishing ' + JSON.stringify(CvEventType[event.type]) + ' to ' + listener); listener(event); }); } if (this.isCacheEnabled() && shouldCache && event.resourceId) { this.cacheObjectAt(event.resourceId, event); } } publishError(message:string, messageObj?:string):void { const event:CvEvent = { type: CvEventType.MESSAGE, eventObj: { message: message, messageObj: messageObj, type: CvMessageType.ERROR } } this.publish(event, false); } cacheObjectAt(id:string, obj:any) { this._cache[id] = obj; } removeFromCache(key:string) { if(this.isCacheEnabled()) { delete this._cache[key]; } } subscribe(listener:CvListener, eventType:CvEventType):void { let listenerArray:Array> = this._listenerMap[eventType]; if (!listenerArray) { listenerArray = []; this._listenerMap[eventType] = listenerArray; } if (listenerArray.indexOf(listener) < 0) { listenerArray.push(listener); } } removeAllListeners():void { this._listenerMap = []; } unsubscribe(listener:CvListener):void { for (const eventType in this._listenerMap) { const listenerArray:Array> = this._listenerMap[eventType]; if (listenerArray) { var index = listenerArray.indexOf(listener); if (index > -1) { listenerArray.splice(index, 1); } } } } } /** * This is a mechanism used throughout the framework * A generalized interface for allowing two components to communicate 'state' changes * i.e. a workbench display and a workbench selector menu * The following compose this mechanism: {@link CvValueListener}, {@link CvValueProvider}, {@link CvValueAdapter} */ export interface CvValueListener { (value:T):void; } export interface CvValueProvider { subscribe(updateListener:CvValueListener):void; } export class CvValueAdapter implements CvValueProvider { private _value:T; private _subscriberArray:Array> = []; /* Create a listener that can be 'handed off' to components that are the SOURCE of events, and that expect a listener. When this listener is notified, it will notify the rest of the listeners in the chain - i.e. the one's that have 'subscribed' to this provider */ getDelegateValueListener():CvValueListener { return ((value:T) => { this._value = value; this._subscriberArray.forEach((updateListener:CvValueListener)=> { updateListener(value) }); }); } /** * @deprecated since 2.0.32 Use getNotifyFunction() instead */ createValueListener():CvValueListener { return this.getDelegateValueListener(); } subscribe(updateListener:CvValueListener):void { if (this._subscriberArray.indexOf(updateListener) < 0) { this._subscriberArray.push(updateListener); if(this._value) { updateListener(this._value); } } } } /** Utilities */ export class CvNavigationResultUtil { static determineType(navRequest:NavRequest):CvNavigationResultType { if(navRequest instanceof FormContext) { return CvNavigationResultType.FORM; } else if(navRequest instanceof WebRedirection) { return CvNavigationResultType.URL; } else { return CvNavigationResultType.NULL; } } static publishNavigation(catavolt:AppContext, eventRegistry:CvEventRegistry, navRequest:NavRequest, actionId:string, workbenchId:string, navTarget:string, navigationListeners:Array<(event:CvEvent)=>void>, sourceIsDestroyed:boolean, noTransition:boolean):CvEvent { const resourceId = CvResourceManager.resourceIdForObject(navRequest, catavolt); const e:CvEvent = { type: CvEventType.NAVIGATION, resourceId: resourceId, eventObj: { navRequest: navRequest, actionId: actionId, workbenchId: workbenchId, navTarget: navTarget, noTransition: noTransition, sourceIsDestroyed: sourceIsDestroyed, type: CvNavigationResultUtil.determineType(navRequest) } }; eventRegistry.publish(e, CvResourceManager.shouldCacheResult(eventRegistry)); if(navigationListeners){ navigationListeners.forEach((listener)=> { listener(e) }); } return e; } } export class CvSessionManager { static removeSession() { if(typeof(sessionStorage) != 'undefined') { sessionStorage.removeItem('catavolt::session'); } } static storeSession(sessionContext:SessionContext) { if(typeof(sessionStorage) != 'undefined') { sessionStorage.setItem('catavolt::session', JSON.stringify(sessionContext)); } } static getSession():SessionContext { if(typeof(sessionStorage) != 'undefined') { const sessionStr = sessionStorage.getItem('catavolt::session'); if (sessionStr) { const sessionObj = JSON.parse(sessionStr); if (sessionObj) { const systemContext:SystemContextImpl = new SystemContextImpl(sessionObj.systemContext._urlString, sessionObj.systemContext._appVersion); return new SessionContextImpl(sessionObj.sessionHandle, sessionObj.userName, sessionObj.currentDivision, sessionObj.serverVersion, systemContext, sessionObj.tenantId); } else { return null; } } else { return null; } } else { return null; } } static updateSession(catavolt:AppContext, eventRegistry:CvEventRegistry):Future { if (typeof(sessionStorage) != 'undefined') { const sessionContext = this.getSession(); if (sessionContext) { const refreshFuture:Future = catavolt.refreshContext(sessionContext); refreshFuture.onComplete(appWinDefTry=> { if (appWinDefTry.isFailure) { const event:CvEvent = { type: CvEventType.MESSAGE, eventObj: { message: 'Update session failed', messageObj: appWinDefTry.failure, type: CvMessageType.ERROR } } eventRegistry.publish(event, false); } else { const event:CvEvent = { type: CvEventType.SESSION_UPDATE, resourceId: CvResourceManager.resourceIdForObject(appWinDefTry.success, catavolt), eventObj: {appWinDef: appWinDefTry.success} }; eventRegistry.publish(event, CvResourceManager.shouldCacheResult(eventRegistry)); } }); return refreshFuture; } else { return Future.createFailedFuture('CvReact::updateSession', 'no persistent session found'); } } else { return Future.createFailedFuture('CvReact::updateSession', 'sessionStorage not supported'); } } } export class CvResourceManager { private static PARAM_DELIM:string = '~'; static deserializeRedirection(token:string):{redirection:DialogRedirection, actionSource:ActionSource} { const [handleValue, sessionHandle, dialogMode, dialogType, objectId, actionId, actionObjId, actionType] = token.split(CvResourceManager.PARAM_DELIM); let actionSource:ActionSource = null; if(actionType && actionType === 'ca') { actionSource = new ContextAction(actionId, actionObjId, null); } else if (actionType && actionType === 'wla'){ actionSource = new WorkbenchLaunchAction(actionId, actionObjId, '','',''); } return {redirection: new DialogRedirection(new DialogHandle(Number(handleValue), sessionHandle), dialogType, dialogMode, null, objectId, false, null, null, {}, {}), actionSource: actionSource} } static shouldCacheResult(eventRegistry:CvEventRegistry):boolean { //cache everything for now, can later set this preference component-by-component via a property return eventRegistry.isCacheEnabled(); } static resourceIdForObject(o:any, catavolt:AppContext):string { if (o instanceof FormContext) { const formContext:FormContext = o; return CvResourceManager.serializeRedirection(formContext.paneDef.dialogRedirection, formContext.actionSource); } else if (o instanceof AppWinDef) { if (catavolt.sessionContextTry.isSuccess) { //right now the 'windowId' is the session handle. may need to change this if want to have multiple windows... return catavolt.sessionContextTry.success.sessionHandle; } else { return null; } } else if (o instanceof WebRedirection) { const webRedirection:WebRedirection = o; return 'ext_' + StringUtil.hashCode(webRedirection.webURL); } else { Log.debug('No resourceId found for object'); return null; } } /** * @private * * This is an attempt to preserve a redirection and all it's state along with the action source, * in a single 'token' that can be used on the URL. This along with the session information, are used to make a * 'stateless' transition to a new URL. * The sdk and server require quite a bit of state to be retained by the client in order to 'rebuild' a navigation. * * @param redirection * @param actionSource * @returns {string} */ private static serializeRedirection(redirection:DialogRedirection, actionSource:ActionSource):string { let [actionId, actionObjId, actionType] = ['', '', '']; if(actionSource instanceof ContextAction) { actionId = (actionSource as ContextAction).actionId; actionObjId = (actionSource as ContextAction).objectId; actionType = 'ca'; } else if (actionSource instanceof WorkbenchLaunchAction) { actionId = (actionSource as WorkbenchLaunchAction).id; actionObjId = (actionSource as WorkbenchLaunchAction).workbenchId; actionType = 'wla' } const dialogHandle = redirection.dialogHandle; const PARAM_DELIM = CvResourceManager.PARAM_DELIM; return (dialogHandle.handleValue ? dialogHandle.handleValue : '') + PARAM_DELIM + (dialogHandle.sessionHandle ? dialogHandle.sessionHandle : '') + PARAM_DELIM + (redirection.dialogMode ? redirection.dialogMode : '') + PARAM_DELIM + (redirection.dialogType ? redirection.dialogType : '') + PARAM_DELIM + (redirection.objectId ? redirection.objectId : '') + PARAM_DELIM + actionId + PARAM_DELIM + actionObjId + PARAM_DELIM + actionType; } } export class ColorUtil { public static toColor(c:{red:number, green:number, blue:number, alpha:number }):string { return "rgba(" + [c.red, c.green, c.blue, c.alpha].join(",") + ")"; } public static toColorFromNum(num):string { num >>>= 0; var b = num & 0xFF, g = (num & 0xFF00) >>> 8, r = (num & 0xFF0000) >>> 16, a = ( (num & 0xFF000000) >>> 24 ) / 255 ; return "rgba(" + [r, g, b, a].join(",") + ")"; } public static toColorFromNumWithAlpha(num, a):string { num >>>= 0; var b = num & 0xFF, g = (num & 0xFF00) >>> 8, r = (num & 0xFF0000) >>> 16; return "rgba(" + [r, g, b, a].join(",") + ")"; } } export class DateUtil { public static toISOFormatNoOffset(d:Date):string { // Build the ISO string because toISOString() will adjust for timezone, which we don't want. // d.toISOString(); - This will adjust the date based on timezone which is undesirable. let a = d.getFullYear() + "-"; let x = d.getMonth() + "-"; if (x.length == 2) { x = "0" + x; } a += x; x = d.getDate() + ""; if (x.length == 1) { x = "0" + x; } a += x; return a; } } /** * CvImage Action Components * A CvImage has a property for actions. An action (CvImageAction) simply represents a button with a button style * and a parameterless callback action to be executed when the button is pressed. * * A CvImageProducer is a candidate representation of a CvAction. It has flags to determine when a CvAction should be * created or not (i.e. you can't delete an image if there is not already an image). UI will configure a set of * CvImageProducers that are appopriate for the UI. A signature control never supports a delete, while an image * control might support create/delete/undo. * * A CvImagePackage is an abstract class with 5 concrete subclasses. An instance of CvImagePackage is the value to * be set on the CvImageProducer call back listener. */ export enum CvImageExistenceState { Missing, Present, Deleted }; export interface CvImageAction { callback:()=>void; buttonClassName:string; } export interface CvImageProducer { prodImageCB?:(listener:CvValueListener)=>void; buttonClassName:string; includeWhenImageisPresent:boolean; includeWhenImageIsDeleted:boolean; includeWhenImageIsMissing:boolean; } export class CvImagePackage { public isUrl():boolean { return false } public isDelete():boolean { return false } public isPick():boolean { return false } public isUndo():boolean { return false } public isRedo():boolean { return false } } export class CvImagePackageUrl extends CvImagePackage { constructor(url:any) { super(); this._url = url; } private _url:string; public get url():string { return this._url; } public isUrl():boolean { return true } } export class CvImagePackagePick extends CvImagePackage { public isPick():boolean { return true } } export class CvImagePackageUndo extends CvImagePackage { public isUndo():boolean { return true } } export class CvImagePackageRedo extends CvImagePackage { public isRedo():boolean { return true } } export class CvImagePackageDelete extends CvImagePackage { public isDelete():boolean { return true } } export class ImageUtil { /** * Conserve aspect ratio of the orignal region. Useful when shrinking/enlarging * images to fit into a certain area. * * In most cases this is not needed as the object-fit:contain CSS will obtain * the desired result. * * @param {Number} srcWidth Source area width * @param {Number} srcHeight Source area height * @param {Number} maxWidth Fittable area maximum available width * @param {Number} maxHeight Fittable area maximum available height * @return {Object} { width, heigth } */ public static calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) { let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); return { width: srcWidth*ratio, height: srcHeight*ratio }; } } export class UIUtil { /* Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for N milliseconds. If `immediate` is passed, trigger the function on the leading edge, instead of the trailing. Note: to use debounce with a React event handler (w/ SyntheticEvent), see http://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html */ public static debounce(func:Function, wait:number, immediate:boolean = false):Function { let timeout; return function() { const context = this, args = arguments; const later = function() { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }; }