Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stevenic/4.6 dialog parity #1384

Merged
merged 21 commits into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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