cv-dialog-sdk
Version:
Catavolt Dialog Javascript API
492 lines (447 loc) • 17.4 kB
text/typescript
import {CatavoltApi} from '../dialog/CatavoltApi';
import {StreamConsumer} from '../io/StreamConsumer';
import {Base64} from '../util/Base64';
import {ActionParameters} from './ActionParameters';
import {Attachment} from './Attachment';
import {DialogException} from './DialogException';
import {ErrorMessage} from './ErrorMessage';
import {LargeProperty} from './LargeProperty';
import {Menu} from './Menu';
import {Property} from './Property';
import {PropertyDef} from './PropertyDef';
import {PropertyFormatter} from './PropertyFormatter';
import {ReadLargePropertyParameters} from './ReadLargePropertyParameters';
import {Record} from './Record';
import {RecordDef} from './RecordDef';
import {Redirection} from './Redirection';
import {ReferringDialog} from './ReferringDialog';
import {ReferringObject} from './ReferringObject';
import {DialogMode, DialogModeEnum, DialogType, TypeNames, ViewMode, ViewModeEnum} from './types';
import {View} from './View';
import {ViewDescriptor} from './ViewDescriptor';
import {WriteLargePropertyParameters} from './WriteLargePropertyParams';
/**
* Top-level class, representing a Catavolt 'Dialog' definition.
* All Dialogs have a composite {@link View} definition along with a single record
* or a list of records. See {@Record}
*/
export abstract class Dialog {
// statics
public static SEARCH_DIALOG_CLASS = 'SearchQueryModel';
public static BINARY_CHUNK_SIZE = 128 * 1024; // size in byes for 'read' operation
private static CHAR_CHUNK_SIZE = 128 * 1000; // size in chars for encoded 'write' operation
public readonly availableViews: ViewDescriptor[];
public readonly domainClassName: string;
public readonly children: Dialog[] = [];
public readonly description: string;
public readonly dialogClassName: string;
public dialogMode: DialogMode;
public readonly header: View;
public readonly id: string;
public recordDef: RecordDef;
public readonly referringObject: ReferringObject;
public readonly selectedViewId: string;
public readonly sessionId: string;
public readonly tenantId: string;
public readonly type: DialogType;
public readonly view: View;
public readonly viewMode: ViewMode;
// private/protected
private _lastRefreshTime: Date = new Date(0);
private _catavolt: CatavoltApi;
// protected _parentDialog;
/* public methods */
public static isSearchDialog(dialog) {
return dialog.dialogClassName && dialog.dialogClassName.indexOf(Dialog.SEARCH_DIALOG_CLASS) > -1 &&
dialog.view && dialog.view.type === TypeNames.DetailsTypeName;
}
get catavolt(): CatavoltApi {
return this._catavolt;
}
get anyChildNeedsRefresh(): boolean {
return (
this.children &&
this.children.some((dialog: Dialog) => {
return dialog.isRefreshNeeded;
})
);
}
public destroy(): Promise<void> {
return this.catavolt.dialogApi.deleteDialog(this.tenantId, this.sessionId, this.id).then(() => {
this.dialogMode = DialogModeEnum.DESTROYED;
});
}
/**
* Return the error associated with this dialog, if any
* @returns {}
*/
get error(): DialogException {
if (this.hasError) {
return (this.view as ErrorMessage).exception;
} else {
return null;
}
}
/**
* Find a menu def on this dialog with the given actionId
* @param actionId
* @returns {Menu}
*/
public findMenuAt(actionId: string) {
return this.view.findMenuAt(actionId);
}
/**
* Get a string representation of this property suitable for 'reading'
*
* @param {Property} prop
* @param {string} propName
* @returns {string}
*/
public formatForRead(prop: Property, propName: string): string {
return PropertyFormatter.singleton(this._catavolt).formatForRead(prop, this.propDefAtName(propName));
}
/**
* Get a string representation of this property suitable for 'writing'
*
* @param {Property} prop
* @param {string} propName
* @returns {string}
*/
public formatForWrite(prop: Property, propName: string): string {
return PropertyFormatter.singleton(this.catavolt).formatForWrite(prop, this.propDefAtName(propName));
}
/**
* Returns whether or not this dialog loaded properly
* @returns {boolean}
*/
get hasError(): boolean {
return this.view instanceof ErrorMessage;
}
/**
* Returns whether or not this Form is destroyed
* @returns {boolean}
*/
get isDestroyed(): boolean {
return this.dialogMode === DialogModeEnum.DESTROYED || this.isAnyChildDestroyed;
}
/**
* Returns whether or not the data in this dialog is out of date
* @returns {boolean}
*/
get isRefreshNeeded(): boolean {
return this._lastRefreshTime.getTime() < this.catavolt.dataLastChangedTime.getTime();
}
get isReadViewMode(): boolean {
return this.viewMode === ViewModeEnum.READ;
}
/**
* Get the last time this dialog's data was refreshed
* @returns {Date}
*/
get lastRefreshTime(): Date {
return this._lastRefreshTime;
}
/**
* @param time
*/
set lastRefreshTime(time: Date) {
this._lastRefreshTime = time;
}
/**
* Get the all {@link Menu}'s associated with this dialog
* @returns {Array<Menu>}
*/
get menu(): Menu {
return this.view.menu;
}
public openViewWithId(viewId: string): Promise<Dialog> {
return this.catavolt.dialogApi
.changeView(this.tenantId, this.sessionId, this.id, viewId)
.then((dialog: Dialog) => {
// any new dialog needs to be initialized with the Catavolt object
dialog.initialize(this.catavolt);
this.updateSettingsWithNewDialogProperties(dialog.referringObject);
return dialog;
});
}
public openView(targetViewDescriptor: ViewDescriptor): Promise<Dialog> {
return this.openViewWithId(targetViewDescriptor.id);
}
/**
* Get the title of this dialog
* @returns {string}
*/
get paneTitle(): string {
let title = this.view.findTitle();
if (!title) {
title = this.description;
}
return title;
}
/**
* Parses a value to prepare for 'writing' back to the server
* @param formattedValue
* @param propName
* @returns {}
*/
public parseValue(formattedValue: any, propName: string): any {
return PropertyFormatter.singleton(this._catavolt).parse(formattedValue, this.propDefAtName(propName));
}
/**
* Get the property definition for a property name
* @param propName
* @returns {PropertyDef}
*/
public propDefAtName(propName: string): PropertyDef {
return this.recordDef.propDefAtName(propName);
}
/**
* Read all the large property values into memory in this {@link Record}
*
* @param {string} recordId
* @returns {Promise<LargeProperty[]>}
*/
public readLargeProperties(recordId: string): Promise<LargeProperty[]> {
return Promise.all(
this.recordDef.propertyDefs
.filter((propDef: PropertyDef) => {
return propDef.isLargePropertyType;
})
.map((propDef: PropertyDef) => {
return this.readLargeProperty(propDef.propertyName, recordId);
})
);
}
/**
* Read a large property into memory
*
* @param {string} propertyName
* @param {string} recordId
* @returns {Promise<LargeProperty>}
*/
public readLargeProperty(propertyName: string, recordId?: string): Promise<LargeProperty> {
return this.loadLargeProperty(propertyName, recordId);
}
/**
* Stream the encoded chunks of a large property without retaining them
* The streamConsumer will receive Base64 encoded chunks with callbacks. hasMore will
* be false with the final chunk.
* @param {StreamConsumer} streamConsumer
* @param {string} propertyName
* @param {string} recordId
* @returns {Promise<LargeProperty>}
*/
public streamLargeProperty(
streamConsumer: StreamConsumer,
propertyName: string,
recordId?: string
): Promise<LargeProperty> {
return this.loadLargeProperty(propertyName, recordId, streamConsumer);
}
/*
get parentDialog():Dialog {
return this._parentDialog;
}
*/
/**
* Get the all {@link ViewDescriptor}'s associated with this Form
* @returns {Array<ViewDescriptor>}
*/
get viewDescs(): ViewDescriptor[] {
return this.availableViews;
}
public initialize(catavolt: CatavoltApi) {
this._catavolt = catavolt;
if (this.children) {
this.children.forEach((child: Dialog) => {
// @TODO add this if needed
// child._parentDialog = this;
child.initialize(catavolt);
});
}
}
protected invokeMenuActionWithId(actionId: string, actionParams: ActionParameters): Promise<Redirection> {
return this.catavolt.dialogApi
.performAction(this.tenantId, this.sessionId, this.id, actionId, actionParams)
.then((result: Redirection) => {
// @TODO - update relevant referring dialog settings on 'this' dialog
this.updateSettingsWithNewDialogProperties(result.referringObject);
if (result.refreshNeeded) {
this.catavolt.dataLastChangedTime = new Date();
}
return result;
});
}
/**
* Perform this action associated with the given Menu on this dialog.
* The targets array is expected to be an array of object ids.
* @param {Menu} menu
* @param {ActionParameters} actionParams
* @returns {Promise<{actionId: string} | Redirection>}
*/
protected invokeMenuAction(menu: Menu, actionParams: ActionParameters): Promise<Redirection> {
return this.invokeMenuActionWithId(menu.actionId, actionParams);
}
protected updateSettingsWithNewDialogProperties(referringObject: ReferringObject) {
if (referringObject) {
if (referringObject.isDialogReferrer()) {
const referringDialog:ReferringDialog = referringObject as ReferringDialog;
if(referringDialog.dialogMode) {
// @TODO - remove the uppercase conversion once all DialogModes come back from server as uppercase
this.dialogMode = referringDialog.dialogMode.toUpperCase() as DialogMode;
}
}
}
}
// protected abstract
protected abstract getProperty(params: ReadLargePropertyParameters, propertyName?: string): Promise<LargeProperty>;
/* @TODO */
protected writeAttachment(attachment: Attachment): Promise<void> {
/*
return DialogService.addAttachment(this.dialogRedirection.dialogHandle, attachment, this.session);
*/
return Promise.resolve(null);
}
protected writeAttachments(record: Record): Promise<void[]> {
return Promise.all(
record.properties
.filter((prop: Property) => {
return prop.value instanceof Attachment;
})
.map((prop: Property) => {
const attachment: Attachment = prop.value as Attachment;
return this.writeAttachment(attachment);
})
);
}
/**
* Write all Binary values in this {@link Record} back to the server
*
* @param {Record} record
* @returns {Promise<void[]>}
*/
protected writeLargeProperties(record: Record): Promise<void[]> {
return Promise.all(
record.properties
.filter((prop: Property) => {
return this.propDefAtName(prop.name).isLargePropertyType;
})
.map((prop: Property) => {
return this.writeLargeProperty(prop.name, prop.value as LargeProperty);
})
);
}
protected writeLargeProperty(propertyName: string, largeProperty: LargeProperty): Promise<void> {
// This is a delete
if (!largeProperty || !largeProperty.encodedData) {
return this.catavolt.dialogApi
.writeProperty(this.tenantId, this.sessionId, this.id, propertyName, {
append: false,
encodedData: null,
type: TypeNames.WriteLargePropertyParameters
})
.then(() => Promise.resolve());
}
const data = Base64.decodeString(largeProperty.encodedData);
const f: (prt: number) => Promise<void> = (ptr: number) => {
if (ptr < data.length) {
const segment: string =
ptr + Dialog.CHAR_CHUNK_SIZE <= data.length
? data.substr(ptr, Dialog.CHAR_CHUNK_SIZE)
: data.substring(ptr);
const params: WriteLargePropertyParameters = {
append: ptr !== 0,
encodedData: Base64.encodeString(segment),
type: TypeNames.WriteLargePropertyParameters
};
return this.catavolt.dialogApi
.writeProperty(this.tenantId, this.sessionId, this.id, propertyName, params)
.then(() => {
return f(ptr + Dialog.CHAR_CHUNK_SIZE);
});
} else {
return Promise.resolve();
}
};
return f(0);
}
/**
* @private
* @returns {boolean}
*/
private get isAnyChildDestroyed(): boolean {
return (
this.children &&
this.children.some((dialog: Dialog) => {
return dialog.isDestroyed;
})
);
}
/**
* Read a large property into memory or stream it, if a streamConsumer is provided
* @param {string} propertyName
* @param {string} recordId
* @param {StreamConsumer} streamConsumer
* @returns {Promise<LargeProperty>}
*/
private loadLargeProperty(
propertyName: string,
recordId: string,
streamConsumer?: StreamConsumer
): Promise<LargeProperty> {
return Dialog.loadLargeProperty(this.getProperty.bind(this), streamConsumer, propertyName, recordId);
}
/**
* Read a large property into memory or stream it, if a streamConsumer is provided
* The actual service call that retrieves the result is delegate to the 'getPropertyFn'
* @param {(params: ReadLargePropertyParameters, propertyName?: string) => Promise<LargeProperty>} getPropertyFn
* @param {StreamConsumer} streamConsumer
* @param {string} propertyName
* @param {string} recordId
* @returns {Promise<LargeProperty>}
*/
public static loadLargeProperty(
getPropertyFn: (params: ReadLargePropertyParameters, propertyName?: string) => Promise<LargeProperty>,
streamConsumer?: StreamConsumer,
propertyName?: string,
recordId?: string
): Promise<LargeProperty> {
let sequence: number = 0;
let resultBuffer: string = '';
const f: (largeProperty: LargeProperty) => Promise<LargeProperty> = async (largeProperty: LargeProperty) => {
streamConsumer &&
(await streamConsumer({ done: !largeProperty.hasMore, value: largeProperty.encodedData }));
if (largeProperty.hasMore) {
if (!streamConsumer) {
resultBuffer += Base64.decodeString(largeProperty.encodedData);
}
const params: ReadLargePropertyParameters = {
maxBytes: Dialog.BINARY_CHUNK_SIZE,
sequence: ++sequence,
recordId,
type: TypeNames.ReadLargePropertyParameters
};
return getPropertyFn(params, propertyName).then(f);
} else {
if (resultBuffer) {
resultBuffer += Base64.decodeString(largeProperty.encodedData);
return Promise.resolve<LargeProperty>(
largeProperty.asNewLargeProperty(Base64.encodeString(resultBuffer))
);
} else {
if (streamConsumer) {
return Promise.resolve<LargeProperty>(largeProperty.asNewLargeProperty(null));
}
return Promise.resolve<LargeProperty>(largeProperty.asNewLargeProperty(largeProperty.encodedData));
}
}
};
const initParams: ReadLargePropertyParameters = {
maxBytes: Dialog.BINARY_CHUNK_SIZE,
sequence,
recordId,
type: TypeNames.ReadLargePropertyParameters
};
return getPropertyFn(initParams, propertyName).then(f);
}
}