Skip to content

Commit

Permalink
Stevenic/4.6 dialog parity (#1384)
Browse files Browse the repository at this point in the history
* Added new DialogStateManager

* Added defaultValue to getValue

* Added state manager tests

* Latest changes

* Ported DialogStateManager from C3

* More robust path parsing...

* Added event emitting support

Also implemented logic to cascade cancel to parent dialogs

* Updated dialog set to support auto id's and dependencies

also replaced "null" with "undefined" htroughout code

* Updated unit tests

* Added unit test for scopes and path resolvers

Fixed issues as detected.

* Added a bunch of dialog state tests

* Added additional unit tests

- Increased code coverage for DialogStateManager
- Fixed issue detected from unit tests.

* Tweaked settings test

* Made DialogContainer.dialogs public

* Fixed code review comment.
  • Loading branch information
Stevenic authored Nov 12, 2019
1 parent 72da85b commit f9e805b
Show file tree
Hide file tree
Showing 42 changed files with 3,193 additions and 112 deletions.
4 changes: 3 additions & 1 deletion libraries/botbuilder-dialogs/.nycrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"**/node_modules/**",
"**/tests/**",
"**/coverage/**",
"**/*.d.ts"
"**/*.d.ts",
"lib/choices/modelResult.js",
"lib/memory/pathResolvers/pathResolver.js"
],
"reporter": [
"html"
Expand Down
4 changes: 2 additions & 2 deletions libraries/botbuilder-dialogs/src/choices/choiceFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ export class ChoiceFactory {
type: ActionTypes.ImBack,
value: choice.value
} as CardAction));
const attachment = CardFactory.heroCard(null, text, null, buttons);
const attachment = CardFactory.heroCard(undefined, text, undefined, buttons);

return MessageFactory.attachment(attachment, null, speak, InputHints.ExpectingInput) as Activity;
return MessageFactory.attachment(attachment, undefined, speak, InputHints.ExpectingInput) as Activity;
}


Expand Down
16 changes: 3 additions & 13 deletions libraries/botbuilder-dialogs/src/componentDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/
import { TurnContext, BotTelemetryClient, NullTelemetryClient } from 'botbuilder-core';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog';
import { DialogContext, DialogState } from './dialogContext';
import { DialogSet } from './dialogSet';
import { DialogContext } from './dialogContext';
import { DialogContainer } from './dialogContainer';

const PERSISTED_DIALOG_STATE = 'dialogs';

Expand Down Expand Up @@ -68,7 +68,7 @@ const PERSISTED_DIALOG_STATE = 'dialogs';
* ```
* @param O (Optional) options that can be passed into the `DialogContext.beginDialog()` method.
*/
export class ComponentDialog<O extends object = {}> extends Dialog<O> {
export class ComponentDialog<O extends object = {}> extends DialogContainer<O> {

/**
* ID of the child dialog that should be started anytime the component is started.
Expand All @@ -77,7 +77,6 @@ export class ComponentDialog<O extends object = {}> extends Dialog<O> {
* This defaults to the ID of the first child dialog added using [addDialog()](#adddialog).
*/
protected initialDialogId: string;
private dialogs: DialogSet = new DialogSet(null);

public async beginDialog(outerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
// Start the inner dialog.
Expand Down Expand Up @@ -155,15 +154,6 @@ export class ComponentDialog<O extends object = {}> extends Dialog<O> {
return this;
}

/**
* Finds a child dialog that was previously added to the component using
* [addDialog()](#adddialog).
* @param dialogId ID of the dialog or prompt to lookup.
*/
public findDialog(dialogId: string): Dialog | undefined {
return this.dialogs.find(dialogId);
}

/**
* Creates the inner dialog context
* @param outerDC the outer dialog context
Expand Down
46 changes: 46 additions & 0 deletions libraries/botbuilder-dialogs/src/configurable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Base class for all configurable classes.
*/
export abstract class Configurable {
/**
* Fluent method for configuring the object.
* @param config Configuration settings to apply.
*/
public configure(config: object): this {
for (const key in config) {
if (config.hasOwnProperty(key)) {
const setting = config[key];
if (Array.isArray(setting)) {
if (Array.isArray(this[key])) {
// Apply as an array update
setting.forEach((item) => this[key].push(item));
} else {
this[key] = setting;
}
} else if (typeof setting == 'object') {
if (typeof this[key] == 'object') {
// Apply as a map update
for (const child in setting) {
if (setting.hasOwnProperty(child)) {
this[key][child] = setting[child];
}
}
} else {
this[key] = setting;
}
} else if (setting !== undefined) {
this[key] = setting;
}
}
}
return this;
}
}
151 changes: 143 additions & 8 deletions libraries/botbuilder-dialogs/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { BotTelemetryClient, NullTelemetryClient, TurnContext } from 'botbuilder-core';
import { DialogContext } from './dialogContext';
import { Configurable } from './configurable';

/**
* Tracking information persisted for an instance of a dialog on the stack.
Expand Down Expand Up @@ -86,6 +87,35 @@ export enum DialogTurnStatus {
cancelled = 'cancelled'
}

export interface DialogEvent {
/**
* Flag indicating whether the event will be bubbled to the parent `DialogContext`.
*/
bubble: boolean;

/**
* Name of the event being raised.
*/
name: string;

/**
* Optional. Value associated with the event.
*/
value?: any;
}

export interface DialogConfiguration {
/**
* Static id of the dialog.
*/
id?: string;

/**
* Telemetry client the dialog should use.
*/
telemetryClient?: BotTelemetryClient;
}

/**
* Returned by `Dialog.continueDialog()` and `DialogContext.beginDialog()` to indicate whether a
* dialog is still active after the turn has been processed by the dialog.
Expand Down Expand Up @@ -130,17 +160,14 @@ export interface DialogTurnResult<T = any> {
/**
* Base class for all dialogs.
*/
export abstract class Dialog<O extends object = {}> {
export abstract class Dialog<O extends object = {}> extends Configurable {
private _id: string;

/**
* Signals the end of a turn by a dialog method or waterfall/sequence step.
*/
public static EndOfTurn: DialogTurnResult = { status: DialogTurnStatus.waiting };

/**
* Unique ID of the dialog.
*/
public readonly id: string;

/**
* The telemetry client for logging events.
* Default this to the NullTelemetryClient, which does nothing.
Expand All @@ -149,12 +176,29 @@ export abstract class Dialog<O extends object = {}> {

/**
* Creates a new Dialog instance.
* @param dialogId Unique ID of the dialog.
* @param dialogId Optional. unique ID of the dialog.
*/
constructor(dialogId: string) {
constructor(dialogId?: string) {
super();
this.id = dialogId;
}

/**
* Unique ID of the dialog.
*
* @remarks
* This will be automatically generated if not specified.
*/
public get id(): string {
if (this._id === undefined) {
this._id = this.onComputeId();
}
return this._id;
}

public set id(value: string) {
this._id = value;
}

/**
* Retrieve the telemetry client for this dialog.
Expand Down Expand Up @@ -238,4 +282,95 @@ export abstract class Dialog<O extends object = {}> {
public async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
// No-op by default
}

/// <summary>
/// Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a dialog that the current dialog started.
/// </summary>
/// <param name="dc">The dialog context for the current turn of conversation.</param>
/// <param name="e">The event being raised.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>True if the event is handled by the current dialog and bubbling should stop.</returns>
public async onDialogEvent(dc: DialogContext, e: DialogEvent): Promise<boolean> {
// Before bubble
let handled = await this.onPreBubbleEventAsync(dc, e);

// Bubble as needed
if (!handled && e.bubble && dc.parent != undefined) {
handled = await dc.parent.emitEvent(e.name, e.value, true, false);
}

// Post bubble
if (!handled) {
handled = await this.onPostBubbleEventAsync(dc, e);
}

return handled;
}

/**
* Called before an event is bubbled to its parent.
*
* @remarks
* This is a good place to perform interception of an event as returning `true` will prevent
* any further bubbling of the event to the dialogs parents and will also prevent any child
* dialogs from performing their default processing.
* @param dc The dialog context for the current turn of conversation.
* @param e The event being raised.
* @returns Whether the event is handled by the current dialog and further processing should stop.
*/
protected async onPreBubbleEventAsync(dc: DialogContext, e: DialogEvent): Promise<boolean> {
return false;
}

/**
* Called after an event was bubbled to all parents and wasn't handled.
*
* @remarks
* This is a good place to perform default processing logic for an event. Returning `true` will
* prevent any processing of the event by child dialogs.
* @param dc The dialog context for the current turn of conversation.
* @param e The event being raised.
* @returns Whether the event is handled by the current dialog and further processing should stop.
*/
protected async onPostBubbleEventAsync(dc: DialogContext, e: DialogEvent): Promise<boolean> {
return false;
}

/**
* Called when a unique ID needs to be computed for a dialog.
*
* @remarks
* SHOULD be overridden to provide a more contextually relevant ID. The preferred pattern for
* ID's is `<dialog type>(this.hashedLabel('<dialog args>'))`.
*/
protected onComputeId(): string {
throw new Error(`Dialog.onComputeId(): not implemented.`)
}

/**
* Aids with computing a unique ID for a dialog by computing a 32 bit hash for a string.
*
* @remarks
* The source for this function was derived from the following article:
*
* https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
*
* @param label String to generate a hash for.
* @returns A string that is 15 characters or less in length.
*/
protected hashedLabel(label: string): string {
const l = label.length;
if (label.length > 15)
{
let hash = 0;
for (let i = 0; i < l; i++) {
const chr = label.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32 bit integer
}
label = `${label.substr(0, 5)}${hash.toString()}`;
}

return label;
}
}
32 changes: 32 additions & 0 deletions libraries/botbuilder-dialogs/src/dialogContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Dialog } from './dialog';
import { DialogSet } from './dialogSet';
import { DialogContext } from './dialogContext';

export abstract class DialogContainer<O extends object = {}> extends Dialog<O> {
/**
* The containers dialog set.
*/
public readonly dialogs = new DialogSet(undefined);

/**
* Creates an inner dialog context for the containers active child.
* @param dc Parents dialog context.
* @returns A new dialog context for the active child or `undefined` if there is no active child.
*/
public abstract createChildContext(dc: DialogContext): DialogContext | undefined;

/**
* Finds a child dialog that was previously added to the container.
* @param dialogId ID of the dialog to lookup.
*/
public findDialog(dialogId: string): Dialog | undefined {
return this.dialogs.find(dialogId);
}
}
Loading

0 comments on commit f9e805b

Please sign in to comment.