Skip to content

Commit

Permalink
Switched to returning DialogTurnStatus evrywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
Stevenic committed Aug 31, 2018
1 parent 7943d78 commit 388d456
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 188 deletions.
73 changes: 41 additions & 32 deletions libraries/botbuilder-dialogs/src/componentDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT License.
*/
import { TurnContext } from 'botbuilder-core';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult } from './dialog';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog';
import { DialogContext, DialogState } from './dialogContext';
import { DialogSet } from './dialogSet';

Expand All @@ -16,40 +16,39 @@ const PERSISTED_DIALOG_STATE: string = 'dialogs';
* The `ComponentDialog` class lets you break your bots logic up into components that can be added
* as a dialog to other dialog sets within your bots project or exported and used in other bot
* projects.
* @param R (Optional) type of result that's expected to be returned by the dialog.
* @param O (Optional) options that can be passed into the begin() method.
*/
export class ComponentDialog<R = any, O = {}> extends Dialog {
export class ComponentDialog<O extends object = {}> extends Dialog<O> {
protected initialDialogId: string;
private dialogs: DialogSet = new DialogSet(null);

public async dialogBegin(dc: DialogContext, options?: any): Promise<DialogTurnResult<R>> {
public async dialogBegin(outerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
// Start the inner dialog.
const dialogState: DialogState = { dialogStack: [] };
dc.activeDialog.state[PERSISTED_DIALOG_STATE] = dialogState;
const cdc: DialogContext = new DialogContext(this.dialogs, dc.context, dialogState);
const turnResult: DialogTurnResult<any> = await this.onDialogBegin(cdc, options);
outerDC.activeDialog.state[PERSISTED_DIALOG_STATE] = dialogState;
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState);
const turnResult: DialogTurnResult<any> = await this.onDialogBegin(innerDC, options);

// Check for end of inner dialog
if (turnResult.hasResult) {
if (turnResult.status !== DialogTurnStatus.waiting) {
// Return result to calling dialog
return await dc.end(turnResult.result);
return await this.endComponent(outerDC, turnResult.result);
} else {
// Just signal end of turn
return Dialog.EndOfTurn;
}
}

public async dialogContinue(dc: DialogContext): Promise<DialogTurnResult<R>> {
public async dialogContinue(outerDC: DialogContext): Promise<DialogTurnResult> {
// Continue execution of inner dialog.
const dialogState: any = dc.activeDialog.state[PERSISTED_DIALOG_STATE];
const cdc: DialogContext = new DialogContext(this.dialogs, dc.context, dialogState);
const turnResult: DialogTurnResult<any> = await this.onDialogContinue(cdc);
const dialogState: any = outerDC.activeDialog.state[PERSISTED_DIALOG_STATE];
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState);
const turnResult: DialogTurnResult<any> = await this.onDialogContinue(innerDC);

// Check for end of inner dialog
if (turnResult.hasResult) {
if (turnResult.status !== DialogTurnStatus.waiting) {
// Return result to calling dialog
return await dc.end(turnResult.result);
return await this.endComponent(outerDC, turnResult.result);
} else {
// Just signal end of turn
return Dialog.EndOfTurn;
Expand All @@ -68,17 +67,25 @@ export class ComponentDialog<R = any, O = {}> extends Dialog {
}

public async dialogReprompt(context: TurnContext, instance: DialogInstance): Promise<void> {
// Delegate to inner dialog.
// Forward to inner dialogs
const dialogState: any = instance.state[PERSISTED_DIALOG_STATE];
const cdc: DialogContext = new DialogContext(this.dialogs, context, dialogState);
await this.onDialogReprompt(cdc);
const innerDC: DialogContext = new DialogContext(this.dialogs, context, dialogState);
await innerDC.reprompt();

// Notify component.
await this.onDialogReprompt(context, instance);
}

public async dialogEnd(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
// Notify inner dialog
const dialogState: any = instance.state[PERSISTED_DIALOG_STATE];
const cdc: DialogContext = new DialogContext(this.dialogs, context, dialogState);
await this.onDialogEnd(cdc, reason);
// Forward cancel to inner dialogs
if (reason === DialogReason.cancelCalled) {
const dialogState: any = instance.state[PERSISTED_DIALOG_STATE];
const innerDC: DialogContext = new DialogContext(this.dialogs, context, dialogState);
await innerDC.cancelAll();
}

// Notify component
await this.onDialogEnd(context, instance, reason);
}

protected addDialog<T extends Dialog>(dialog: T): T {
Expand All @@ -88,21 +95,23 @@ export class ComponentDialog<R = any, O = {}> extends Dialog {
return dialog;
}

protected onDialogBegin(dc: DialogContext, options?: any): Promise<DialogTurnResult> {
return dc.begin(this.initialDialogId, options);
protected onDialogBegin(innerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
return innerDC.begin(this.initialDialogId, options);
}

protected async onDialogEnd(dc: DialogContext, reason: DialogReason): Promise<void> {
if (reason === DialogReason.cancelCalled) {
await dc.cancelAll();
}
protected onDialogContinue(innerDC: DialogContext): Promise<DialogTurnResult> {
return innerDC.continue();
}

protected onDialogEnd(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
return Promise.resolve();
}

protected onDialogContinue(dc: DialogContext): Promise<DialogTurnResult> {
return dc.continue();
protected onDialogReprompt(context: TurnContext, instance: DialogInstance): Promise<void> {
return Promise.resolve();
}

protected onDialogReprompt(dc: DialogContext): Promise<void> {
return dc.reprompt();
protected endComponent(outerDC: DialogContext, result: any): Promise<DialogTurnResult> {
return outerDC.end(result);
}
}
38 changes: 25 additions & 13 deletions libraries/botbuilder-dialogs/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,36 @@ export interface DialogInstance<T = any> {

export enum DialogReason {
// A dialog is being started through a call to `DialogContext.begin()`.
beginCalled,
beginCalled = 'beginCalled',

// A dialog is being continued through a call to `DialogContext.continue()`.
continueCalled,
continueCalled = 'continueCalled',

// A dialog ended normally through a call to `DialogContext.end()`.
endCalled,
endCalled = 'endCalled',

// A dialog is ending because its being replaced through a call to `DialogContext.replace()`.
replaceCalled,
replaceCalled = 'replaceCalled',

// A dialog was cancelled as part of a call to `DialogContext.cancelAll()`.
cancelCalled,
cancelCalled = 'cancelCalled',

// A step was advanced through a call to `WaterfallStepContext.next()`.
nextCalled
nextCalled = 'nextCalled'
}

export enum DialogTurnStatus {
// Indicates that there is currently nothing on the dialog stack.
empty = 'empty',

// Indicates that the dialog on top is waiting for a response from the user.
waiting = 'waiting',

// Indicates that the dialog completed successfully, the result is available, and the stack is empty.
complete = 'complete',

// Indicates that the dialog was cancelled and the stack is empty.
cancelled = 'cancelled'
}

/**
Expand All @@ -47,11 +61,8 @@ export enum DialogReason {
* @param T (Optional) type of result returned by the dialog when it calls `dc.end()`.
*/
export interface DialogTurnResult<T = any> {
// If `true` a dialog is still active on the dialog stack.
hasActive: boolean;

// If `true` the dialog that was on the stack just completed and the final [result](#result) is available.
hasResult: boolean;
// Gets or sets the current status of the stack.
status: DialogTurnStatus;

// Final result returned by a dialog that just completed. Can be `undefined` even when [hasResult](#hasResult) is true.
result?: T;
Expand All @@ -62,8 +73,9 @@ export interface DialogTurnResult<T = any> {
*/
export abstract class Dialog<O extends object = {}> {
// Signals the end of a turn by a dialog method or waterfall/sequence step.
// tslint:disable-next-line:variable-name
public static EndOfTurn: DialogTurnResult = { hasActive: true, hasResult: false };
public static EndOfTurn: DialogTurnResult = { status: DialogTurnStatus.waiting };

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

constructor(dialogId: string) {
Expand Down
37 changes: 14 additions & 23 deletions libraries/botbuilder-dialogs/src/dialogContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Licensed under the MIT License.
*/
import { TurnContext, Activity } from 'botbuilder-core';
import { DialogInstance, DialogTurnResult, DialogReason } from './dialog';
import { DialogInstance, DialogTurnResult, DialogTurnStatus, DialogReason } from './dialog';
import { DialogSet } from './dialogSet';
import { PromptOptions } from './prompts';
import { Choice } from './choices';
Expand Down Expand Up @@ -104,16 +104,20 @@ export class DialogContext {
this.stack.push(instance);

// Call dialogs begin() method.
const turnResult = await dialog.dialogBegin(this, options);
return this.verifyTurnResult(turnResult);
return await dialog.dialogBegin(this, options);
}

/**
* Cancels all dialogs on the stack resulting in an empty stack.
*/
public async cancelAll(): Promise<void> {
while (this.stack.length > 0) {
await this.endActiveDialog(DialogReason.cancelCalled);
public async cancelAll(): Promise<DialogTurnResult> {
if (this.stack.length > 0) {
while (this.stack.length > 0) {
await this.endActiveDialog(DialogReason.cancelCalled);
}
return { status: DialogTurnStatus.cancelled };
} else {
return { status: DialogTurnStatus.empty };
}
}

Expand Down Expand Up @@ -172,10 +176,9 @@ export class DialogContext {
if (!dialog) { throw new Error(`DialogContext.continue(): Can't continue dialog. A dialog with an id of '${instance.id}' wasn't found.`) }

// Continue execution of dialog
const turnResult = await dialog.dialogContinue(this);
return this.verifyTurnResult(turnResult);
return await dialog.dialogContinue(this);
} else {
return { hasActive: false, hasResult: false };
return { status: DialogTurnStatus.empty };
}
}

Expand Down Expand Up @@ -205,11 +208,10 @@ export class DialogContext {
if (!dialog) { throw new Error(`DialogContext.end(): Can't resume previous dialog. A dialog with an id of '${instance.id}' wasn't found.`) }

// Return result to previous dialog
const turnResult = await dialog.dialogResume(this, DialogReason.endCalled, result);
return this.verifyTurnResult(turnResult);
return await dialog.dialogResume(this, DialogReason.endCalled, result);
} else {
// Signal completion
return { hasActive: false, hasResult: true, result: result };
return { status: DialogTurnStatus.complete, result: result };
}
}

Expand Down Expand Up @@ -263,16 +265,5 @@ export class DialogContext {
this.stack.pop()
}
}


/** @private helper to ensure the turn result from a dialog looks correct. */
private verifyTurnResult(result: DialogTurnResult): DialogTurnResult {
result.hasActive = this.stack.length > 0;
if (result.hasActive) {
result.hasResult = false;
result.result = undefined;
}
return result;
}
}

24 changes: 12 additions & 12 deletions libraries/botbuilder-dialogs/tests/attachmentPrompt.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { BotState, BotStatePropertyAccessor, ConversationState, MemoryStorage, TestAdapter, TurnContext } = require('botbuilder-core');
const { AttachmentPrompt, DialogSet, DialogState, WaterfallDialog } = require('../');
const { ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core');
const { AttachmentPrompt, DialogSet, DialogTurnStatus } = require('../');
const assert = require('assert');

const answerMessage = { text: `here you go`, type: 'message', attachments: [{ contentType: 'test', content: 'test1' }] };
Expand All @@ -14,9 +14,9 @@ describe('AttachmentPrompt', function() {
const dc = await dialogs.createContext(turnContext);

const results = await dc.continue();
if (!turnContext.responded && !results.hasActive && !results.hasResult) {
if (results.status === DialogTurnStatus.empty) {
await dc.prompt('prompt', { prompt: 'Please send an attachment.' });
} else if (!results.hasActive && results.hasResult) {
} else if (results.status === DialogTurnStatus.complete) {
assert(Array.isArray(results.result) && results.result.length > 0);
const attachment = results.result[0];
await turnContext.sendActivity(`${attachment.content}`);
Expand Down Expand Up @@ -44,9 +44,9 @@ describe('AttachmentPrompt', function() {
const dc = await dialogs.createContext(turnContext);

const results = await dc.continue();
if (!turnContext.responded && !results.hasActive && !results.hasResult) {
if (results.status === DialogTurnStatus.empty) {
await dc.prompt('prompt', { prompt: 'Please send an attachment.' });
} else if (!results.hasActive && results.hasResult) {
} else if (results.status === DialogTurnStatus.complete) {
assert(Array.isArray(results.result) && results.result.length > 0);
const attachment = results.result[0];
await turnContext.sendActivity(`${attachment.content}`);
Expand Down Expand Up @@ -76,9 +76,9 @@ describe('AttachmentPrompt', function() {
const dc = await dialogs.createContext(turnContext);

const results = await dc.continue();
if (!turnContext.responded && !results.hasActive && !results.hasResult) {
if (results.status === DialogTurnStatus.empty) {
await dc.prompt('prompt', { prompt: 'Please send an attachment.', retryPrompt: 'Please try again.' });
} else if (!results.hasActive && results.hasResult) {
} else if (results.status === DialogTurnStatus.complete) {
assert(Array.isArray(results.result) && results.result.length > 0);
const attachment = results.result[0];
await turnContext.sendActivity(`${attachment.content}`);
Expand Down Expand Up @@ -114,9 +114,9 @@ describe('AttachmentPrompt', function() {
const dc = await dialogs.createContext(turnContext);

const results = await dc.continue();
if (!turnContext.responded && !results.hasActive && !results.hasResult) {
if (results.status === DialogTurnStatus.empty) {
await dc.prompt('prompt', { prompt: 'Please send an attachment.', retryPrompt: 'Please try again.' });
} else if (!results.hasActive && results.hasResult) {
} else if (results.status === DialogTurnStatus.complete) {
assert(Array.isArray(results.result) && results.result.length > 0);
const attachment = results.result[0];
await turnContext.sendActivity(`${attachment.content}`);
Expand Down Expand Up @@ -153,9 +153,9 @@ describe('AttachmentPrompt', function() {
const dc = await dialogs.createContext(turnContext);

const results = await dc.continue();
if (!turnContext.responded && !results.hasActive && !results.hasResult) {
if (results.status === DialogTurnStatus.empty) {
await dc.begin('prompt');
} else if (!results.hasActive && results.hasResult) {
} else if (results.status === DialogTurnStatus.complete) {
assert(Array.isArray(results.result) && results.result.length > 0);
const attachment = results.result[0];
await turnContext.sendActivity(`${attachment.content}`);
Expand Down

0 comments on commit 388d456

Please sign in to comment.