diff --git a/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetExpressionEvaluator.test.ts b/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetExpressionEvaluator.test.ts new file mode 100644 index 0000000000..db6f31f9a3 --- /dev/null +++ b/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetExpressionEvaluator.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + evaluateWidgetProp, + ActionContextKey, +} from '../../../src/adaptive-flow-renderer/utils/expression/widgetExpressionEvaluator'; + +const evaluate = evaluateWidgetProp; +describe('evaluateWidgetProp()', () => { + it('can evaluate string interpolation pattern.', () => { + expect(evaluate('${a}, ${b}', { a: 1, b: 2 })).toEqual('1, 2'); + }); + + it('return origin input when input non-expression string.', () => { + ['hello', 'action.x', '= Result Value', '= ActivityId'].forEach((x) => { + expect(evaluate(x, {}, ActionContextKey)).toEqual(x); + }); + }); + + it('can evaluate basic value access pattern.', () => { + expect(evaluate('=action.x', { action: { x: 1 } })).toEqual(1); + + // Notes: Expression engine behavior. 'null' will be read as 'undefined' + expect(evaluate('=action.x', { action: { x: null } })).toEqual(undefined); + expect(evaluate('=action.x', { action: {} })).toEqual(undefined); + expect(evaluate('=action.x', { action: { x: undefined } })).toEqual(undefined); + expect(evaluate('=action.x', { action: { x: false } })).toEqual(false); + + // Convert to string + expect(evaluate('=string(action.x)', { action: { x: null } })).toEqual(undefined); + expect(evaluate('=string(action.x)', { action: { x: undefined } })).toEqual(undefined); + expect(evaluate('=string(action.x)', { action: { x: false } })).toEqual('false'); + + // Case-insensitive + expect(evaluate('=action.x', { action: { x: 'x' } })).toEqual('x'); + expect(evaluate('=action.x', { action: { X: 'x' } })).toEqual('x'); + }); + + it('can evaluate expressions with foreach', () => { + expect(evaluate('=foreach(a.items, x=>x)', { a: { items: [] } })).toEqual([]); + expect(evaluate('=foreach(a.items, x=>x)', { a: { items: [1, 2] } })).toEqual([1, 2]); + expect(evaluate('=foreach(a.items, x=>x+1)', { a: { items: [1, 2] } })).toEqual([2, 3]); + expect(evaluate('=foreach(a.items, x=>x)', { a: { items: ['1'] } })).toEqual(['1']); + expect(evaluate('=foreach(a.items, x=>x+1)', { a: { items: ['1'] } })).toEqual(['11']); + expect(evaluate('=foreach(a.items, x=>x+"1")', { a: { items: ['1'] } })).toEqual(['11']); + }); + + it('can evaluate expressions with if', () => { + expect(evaluate('=if(a.x, "1", "2")', { a: { x: true } })).toEqual('1'); + expect(evaluate('=if(a.x, "1", "2")', { a: { X: true } })).toEqual('1'); + expect(evaluate('=if(a.x, "1", "2")', { a: { x: '' } })).toEqual('1'); + expect(evaluate('=if(a.x, "1", "2")', { a: { x: 0 } })).toEqual('1'); + expect(evaluate('=if(a.x<1, "1", "2")', { a: { x: 0 } })).toEqual('1'); + + expect(evaluate('=if(a.x, "1", "2")', { a: { x: false } })).toEqual('2'); + expect(evaluate('=if(a.x, "1", "2")', { a: {} })).toEqual('2'); + expect(evaluate('=if(a.x, "1", "2")', { a: { x: null } })).toEqual('2'); + expect(evaluate('=if(a.x, "1", "2")', { a: { x: undefined } })).toEqual('2'); + }); + + it('can evaluate string template pattern.', () => { + expect(evaluate('=concat(a.x, " and ", a.y)', { a: { x: 'x', y: 'y' } })).toEqual('x and y'); + expect(evaluate('=concat(a.x, " and ", a.y)', { a: { X: 'X', Y: 'Y' } })).toEqual('X and Y'); + expect(evaluate('=concat(a.x, " and ", a.y)', { a: { x: 1 } })).toEqual('1 and '); + expect(evaluate('=concat(string(a.x), " and ", string(a.y))', { a: { x: { val: 1 }, y: [1] } })).toEqual( + '{"val":1} and [1]' + ); + }); + + describe('can evaluate real Flow use case', () => { + it('in Foreach.', () => { + expect( + evaluate('=concat("Each value in ", coalesce(action.itemsProperty, "?"))', { + action: { itemsProperty: 'user.names' }, + }) + ).toEqual('Each value in user.names'); + expect(evaluate('=concat("Each value in ", coalesce(action.itemsProperty, "?"))', { action: {} })).toEqual( + 'Each value in ?' + ); + }); + + it('in SetProperties.', () => { + expect( + evaluate('=foreach(action.assignments, x => concat(x.property, " : " ,x.value))', { + action: { + assignments: [ + { property: 'a', value: 1 }, + { property: 'b', value: '2' }, + ], + }, + }) + ).toEqual(['a : 1', 'b : 2']); + }); + }); +}); diff --git a/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetRenderer.test.tsx b/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetRenderer.test.tsx index e87c793bb2..5047f26d24 100644 --- a/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetRenderer.test.tsx +++ b/Composer/packages/adaptive-flow/__tests__/adaptive-flow-renderer/utils/widgetRenderer.test.tsx @@ -3,10 +3,9 @@ import React from 'react'; import { render } from '@bfc/test-utils'; -import { WidgetComponent, FlowEditorWidgetMap } from '@bfc/extension-client'; +import { WidgetComponent, FlowEditorWidgetMap, FlowWidget } from '@bfc/extension-client'; import { renderUIWidget, UIWidgetContext } from '../../../src/adaptive-flow-renderer/utils/visual/widgetRenderer'; -import { FlowWidget } from '../../../src/adaptive-flow-renderer/types/flowRenderer.types'; const renderWidget = (schema: FlowWidget, widgetsMap: FlowEditorWidgetMap, context: UIWidgetContext) => { const TestResult = () => renderUIWidget(schema, widgetsMap, context); diff --git a/Composer/packages/adaptive-flow/package.json b/Composer/packages/adaptive-flow/package.json index 7c2cdc9ddc..a0b00ce5e9 100644 --- a/Composer/packages/adaptive-flow/package.json +++ b/Composer/packages/adaptive-flow/package.json @@ -28,6 +28,8 @@ "@bfc/ui-shared": "*", "@emotion/core": "^10.0.27", "@emotion/styled": "^10.0.27", + "adaptive-expressions": "4.10.0-preview-147186", + "botbuilder-lg": "^4.10.0-preview-150886", "create-react-class": "^15.6.3", "d3": "^5.9.1", "dagre": "^0.8.4", diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-editor/AdaptiveFlowEditor.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-editor/AdaptiveFlowEditor.tsx index 9ef83f84c2..61c04ec80c 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-editor/AdaptiveFlowEditor.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-editor/AdaptiveFlowEditor.tsx @@ -8,10 +8,9 @@ import React, { useRef, useMemo, useEffect } from 'react'; import isEqual from 'lodash/isEqual'; import formatMessage from 'format-message'; import { DialogFactory } from '@bfc/shared'; -import { useShellApi, JSONSchema7 } from '@bfc/extension-client'; +import { useShellApi, JSONSchema7, FlowUISchema, FlowWidget } from '@bfc/extension-client'; import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { FlowSchema, FlowWidget } from '../adaptive-flow-renderer/types/flowRenderer.types'; import { NodeEventTypes } from '../adaptive-flow-renderer/constants/NodeEventTypes'; import { AdaptiveDialog } from '../adaptive-flow-renderer/adaptive/AdaptiveDialog'; @@ -105,7 +104,7 @@ const VisualDesigner: React.FC = ({ onFocus, onBlur, schema customSchemas: customActionSchema ? [customActionSchema] : [], }; - const customFlowSchema: FlowSchema = nodeContext.customSchemas.reduce((result, s) => { + const customFlowSchema: FlowUISchema = nodeContext.customSchemas.reduce((result, s) => { const definitionKeys: string[] = Object.keys(s.definitions); definitionKeys.forEach(($kind) => { result[$kind] = { @@ -114,7 +113,7 @@ const VisualDesigner: React.FC = ({ onFocus, onBlur, schema } as FlowWidget; }); return result; - }, {} as FlowSchema); + }, {} as FlowUISchema); const divRef = useRef(null); diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/adaptive/AdaptiveDialog.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/adaptive/AdaptiveDialog.tsx index 4b6a02d17f..c95b098491 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/adaptive/AdaptiveDialog.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/adaptive/AdaptiveDialog.tsx @@ -5,9 +5,8 @@ import { jsx } from '@emotion/core'; import { FC, Fragment } from 'react'; import get from 'lodash/get'; -import { FlowEditorWidgetMap as NodeWidgetMap } from '@bfc/extension-client'; +import { FlowEditorWidgetMap, FlowUISchema } from '@bfc/extension-client'; -import { FlowSchema } from '../types/flowRenderer.types'; import { EditorEventHandler } from '../constants/NodeEventTypes'; import { RendererContext, DefaultRenderers, RendererContextData } from '../contexts/RendererContext'; import builtinSchema from '../configs/builtinSchema'; @@ -31,10 +30,10 @@ export interface AdaptiveDialogProps { onEvent: EditorEventHandler; /** UI schema to define how to render a sdk $kind */ - schema: FlowSchema; + schema: FlowUISchema; /** All available widgets to render a node */ - widgets: NodeWidgetMap; + widgets: FlowEditorWidgetMap; renderers?: Partial; } diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.ts b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.ts new file mode 100644 index 0000000000..0b0be5936c --- /dev/null +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@bfc/shared'; +import { FlowUISchema } from '@bfc/extension-client'; + +const builtinVisualSDKSchema: FlowUISchema = { + default: { + widget: 'ActionHeader', + }, + custom: { + widget: 'ActionHeader', + colors: { theme: '#69797E', color: '#FFFFFF' }, + }, + [SDKKinds.IfCondition]: { + widget: 'IfConditionWidget', + nowrap: true, + judgement: { + widget: 'ActionCard', + body: '=coalesce(action.condition, "")', + }, + }, + [SDKKinds.SwitchCondition]: { + widget: 'SwitchConditionWidget', + nowrap: true, + judgement: { + widget: 'ActionCard', + body: '=coalesce(action.condition, "")', + }, + }, + [SDKKinds.Foreach]: { + widget: 'ForeachWidget', + nowrap: true, + loop: { + widget: 'ActionCard', + body: '=concat("Each value in ", coalesce(action.itemsProperty, "?"))', + }, + }, + [SDKKinds.ForeachPage]: { + widget: 'ForeachWidget', + nowrap: true, + loop: { + widget: 'ActionCard', + body: '=concat("Each page of ", coalesce(action.pageSize, "?"), " in ", coalesce(action.page, "?"))', + }, + }, +}; + +export default builtinVisualSDKSchema; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.tsx deleted file mode 100644 index 778251431c..0000000000 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinSchema.tsx +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { SDKKinds } from '@bfc/shared'; -import formatMessage from 'format-message'; -import React from 'react'; -import get from 'lodash/get'; -import { FixedInfo, SingleLineDiv, ListOverview, PropertyAssignment } from '@bfc/ui-shared'; - -import { FlowSchema, FlowWidget } from '../types/flowRenderer.types'; -import { ObiColors } from '../constants/ElementColors'; - -const BaseInputSchema: FlowWidget = { - widget: 'ActionCard', - body: (data) => data.prompt, -}; - -const builtinVisualSDKSchema: FlowSchema = { - default: { - widget: 'ActionHeader', - }, - custom: { - widget: 'ActionHeader', - colors: { theme: ObiColors.Gray20, color: ObiColors.White }, - }, - [SDKKinds.IfCondition]: { - widget: 'IfConditionWidget', - nowrap: true, - judgement: { - widget: 'ActionCard', - body: (data) => data.condition, - }, - }, - [SDKKinds.SwitchCondition]: { - widget: 'SwitchConditionWidget', - nowrap: true, - judgement: { - widget: 'ActionCard', - body: (data) => data.condition, - }, - }, - [SDKKinds.Foreach]: { - widget: 'ForeachWidget', - nowrap: true, - loop: { - widget: 'ActionCard', - body: (data) => `${formatMessage('Each value in')} {${data.itemsProperty || '?'}}`, - }, - }, - [SDKKinds.ForeachPage]: { - widget: 'ForeachWidget', - nowrap: true, - loop: { - widget: 'ActionCard', - body: (data) => { - const pageSizeString = get(data, 'pageSize', '?'); - const propString = get(data, 'itemsProperty', '?'); - return formatMessage('Each page of {pageSizeString} in {propString}', { pageSizeString, propString }); - }, - }, - }, - [SDKKinds.SendActivity]: { - widget: 'ActionCard', - body: (data) => data.activity, - }, - [SDKKinds.AttachmentInput]: BaseInputSchema, - [SDKKinds.ConfirmInput]: BaseInputSchema, - [SDKKinds.DateTimeInput]: BaseInputSchema, - [SDKKinds.NumberInput]: BaseInputSchema, - [SDKKinds.TextInput]: BaseInputSchema, - [SDKKinds.ChoiceInput]: BaseInputSchema, - [SDKKinds.BeginDialog]: { - widget: 'ActionCard', - body: { - widget: 'DialogRef', - dialog: (data) => data.dialog, - getRefContent: (data) => (dialogRef) => ( - <> - {dialogRef || '?'} {formatMessage('(Dialog)')} - - ), - }, - footer: (data) => - data.property ? ( - <> - {data.property} {formatMessage('= Return value')} - - ) : null, - }, - [SDKKinds.BeginSkill]: { - widget: 'ActionCard', - colors: { theme: ObiColors.DarkBlue, color: ObiColors.White, icon: ObiColors.White }, - icon: 'Library', - body: (data) => ( - - {formatMessage('Host ')} - {data.skillEndpoint || '?'} - - ), - footer: (data) => - data.resultProperty ? ( - <> - {data.resultProperty} - {formatMessage(' = Result')} - - ) : null, - }, - [SDKKinds.ReplaceDialog]: { - widget: 'ActionCard', - body: { - widget: 'DialogRef', - dialog: (data) => data.dialog, - getRefContent: (data) => (dialogRef) => ( - <> - {dialogRef || '?'} {formatMessage('(Dialog)')} - - ), - }, - }, - [SDKKinds.EditArray]: { - widget: 'ActionCard', - body: (data) => ( - <> - {data.changeType || '?'} {data.itemsProperty || '?'} - - ), - footer: (data) => - data.resultProperty ? ( - <> - {data.resultProperty} - {formatMessage(' = Result')} - - ) : null, - }, - [SDKKinds.SetProperty]: { - widget: 'ActionCard', - body: (data) => , - }, - [SDKKinds.SetProperties]: { - widget: 'ActionCard', - body: (data) => ( - } - /> - ), - }, - [SDKKinds.DeleteActivity]: { - widget: 'ActionCard', - header: { - widget: 'ActionHeader', - title: 'Delete activity', - }, - body: (data) => ( - <> - {data.activityId || '?'} - - ), - }, - [SDKKinds.UpdateActivity]: { - widget: 'ActionCard', - header: { - widget: 'ActionHeader', - title: 'Update activity', - }, - body: (data) => data.activity, - }, - [SDKKinds.DeleteProperty]: { - widget: 'ActionCard', - body: (data) => data.property, - }, - [SDKKinds.DeleteProperties]: { - widget: 'ActionCard', - body: (data) => ( - ( - - {item} - - )} - /> - ), - }, - [SDKKinds.CancelAllDialogs]: { - widget: 'ActionCard', - body: (data) => - data.eventName ? ( - <> - {data.eventName || '?'} - {formatMessage(' (Event)')} - - ) : null, - }, - [SDKKinds.EmitEvent]: { - widget: 'ActionCard', - body: (data) => ( - <> - {data.eventName || '?'} - {formatMessage(' (Event)')} - - ), - }, - [SDKKinds.HttpRequest]: { - widget: 'ActionCard', - body: (data) => ( - - {data.method} - {data.url} - - ), - footer: (data) => - data.resultProperty ? ( - <> - {data.resultProperty} - {formatMessage(' = Result property')} - - ) : null, - }, - [SDKKinds.EditActions]: { - widget: 'ActionCard', - body: (data) => data.changeType, - }, - [SDKKinds.QnAMakerDialog]: { - widget: 'ActionCard', - body: (data) => data.hostname, - }, - [SDKKinds.OAuthInput]: { - widget: 'ActionCard', - body: (data) => {data.connectionName}, - footer: (data) => - data.tokenProperty ? ( - <> - {data.tokenProperty} - {formatMessage(' = Token Property')} - - ) : null, - }, - [SDKKinds.TelemetryTrackEvent]: { - widget: 'ActionCard', - header: { - widget: 'ActionHeader', - title: 'Telemetry - Trace Event', - }, - body: (data) => ( - <> - {data.eventName || '?'} - {formatMessage(' (Event)')} - - ), - }, -}; - -export default builtinVisualSDKSchema; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.ts similarity index 77% rename from Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.tsx rename to Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.ts index d86b551e95..240fd0d97f 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/configs/builtinWidgets.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { FlowEditorWidgetMap } from '@bfc/extension-client'; +import { ListOverview } from '@bfc/ui-shared'; import { ActionCard, @@ -11,6 +12,8 @@ import { SwitchConditionWidget, ForeachWidget, ActionHeader, + PropertyDescription, + ResourceOperation, } from '../widgets'; const builtinActionWidgets: FlowEditorWidgetMap = { @@ -21,6 +24,9 @@ const builtinActionWidgets: FlowEditorWidgetMap = { SwitchConditionWidget, ForeachWidget, ActionHeader, + PropertyDescription, + ResourceOperation, + ListOverview, }; export default builtinActionWidgets; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/types/flowRenderer.types.ts b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/types/flowRenderer.types.ts deleted file mode 100644 index 716f2f6400..0000000000 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/types/flowRenderer.types.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { FC, ComponentClass } from 'react'; -import { BaseSchema, SDKKinds } from '@bfc/shared'; - -// TODO: There is another duplication of these types under @bfc/extension/src/types/flowSchema.ts for sharing with plugins. -// Consider reverting the package dependency tree, let @bfc/extension depends on @bfc/adaptive-flow-renderers. - -export enum FlowSchemaBuiltinKeys { - default = 'default', - custom = 'custom', -} - -/** schema */ -export type FlowSchema = { - [key in SDKKinds | FlowSchemaBuiltinKeys]?: FlowWidget; -}; - -export interface FlowWidget { - /** Widget implementation (React Class) or Widget name (string) */ - widget: string | WidgetComponent; - - /** If set to true, output widget will be borderless (usually applied to IfCondition, SwitchCondition) */ - nowrap?: boolean; - - [propKey: string]: FlowWidgetProp; -} - -export type WidgetComponent = FC | ComponentClass; - -export type WidgetEventHandler = (eventName: string, eventData?: any) => void; - -export interface WidgetContainerProps { - id: string; - data: BaseSchema; - onEvent: WidgetEventHandler; - [propKey: string]: any; -} - -export type FlowWidgetProp = Value | PropGenerator | FlowWidget; -type Value = string | number | boolean | undefined | { [key: string]: any }; -type PropGenerator = (data: any) => string | number | object | JSX.Element; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/expression/widgetExpressionEvaluator.ts b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/expression/widgetExpressionEvaluator.ts new file mode 100644 index 0000000000..7c6b6394a2 --- /dev/null +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/expression/widgetExpressionEvaluator.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Templates } from 'botbuilder-lg'; +import { ValueExpression } from 'adaptive-expressions'; + +const evaluateAsLGTemplate = (propValue: string, scope: any) => { + const templateId = 'widgetProp'; + const lgContent = `# ${templateId}\r\n- ${propValue}`; + try { + const lgResult = Templates.parseText(lgContent).evaluate(templateId, scope); + return lgResult ?? propValue; + } catch (err) { + return propValue; + } +}; + +const evaluateAsValueExpression = (propValue: string, scope: any): any => { + try { + const expResult = new ValueExpression(propValue).tryGetValue(scope); + return expResult.value; + } catch (err) { + return propValue; + } +}; + +const StringInterpolationPattern = new RegExp(/\$\{.+\}/); + +export const ActionContextKey = 'action'; + +export const evaluateWidgetProp = (input: string, context: any, requiredContextKey = ''): any => { + if (typeof input !== 'string') return input; + + if (input.startsWith('=') && input.indexOf(requiredContextKey) > -1) { + return evaluateAsValueExpression(input, context); + } + + if (StringInterpolationPattern.test(input)) { + return evaluateAsLGTemplate(input, context); + } + return input; +}; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/WidgetSchemaProvider.ts b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/WidgetSchemaProvider.ts index fb90762863..a8dad1fdcd 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/WidgetSchemaProvider.ts +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/WidgetSchemaProvider.ts @@ -3,20 +3,19 @@ import get from 'lodash/get'; import merge from 'lodash/merge'; - -import { FlowWidget, FlowSchema } from '../../types/flowRenderer.types'; +import { FlowWidget, FlowUISchema } from '@bfc/extension-client'; export class WidgetSchemaProvider { - schema: FlowSchema; + schema: FlowUISchema; /** * @param schemas Schemas to be merged together. Latter ones will override former ones. */ - constructor(...schemas: FlowSchema[]) { + constructor(...schemas: FlowUISchema[]) { this.schema = this.mergeSchemas(schemas); } - private mergeSchemas(orderedSchemas: FlowSchema[]): FlowSchema { + private mergeSchemas(orderedSchemas: FlowUISchema[]): FlowUISchema { if (!Array.isArray(orderedSchemas) || !orderedSchemas.length) return {}; return merge({}, ...orderedSchemas); } diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/widgetRenderer.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/widgetRenderer.tsx index 2fb00e7de9..4734ab1d65 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/widgetRenderer.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/utils/visual/widgetRenderer.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { BaseSchema } from '@bfc/shared'; -import { FlowEditorWidgetMap } from '@bfc/extension-client'; +import { FlowEditorWidgetMap, FlowWidget, FlowWidgetProp, WidgetEventHandler } from '@bfc/extension-client'; -import { FlowWidget, FlowWidgetProp, WidgetEventHandler } from '../../types/flowRenderer.types'; import { Boundary } from '../../models/Boundary'; +import { evaluateWidgetProp, ActionContextKey } from '../expression/widgetExpressionEvaluator'; export interface UIWidgetContext { /** The uniq id of current schema data. Usually a json path. */ @@ -43,13 +43,23 @@ export const renderUIWidget = ( }; const buildWidgetProp = (rawPropValue: FlowWidgetProp, context: UIWidgetContext) => { + // Case 1: For function props, invoke it to transform action data if (typeof rawPropValue === 'function') { const dataTransformer = rawPropValue; const element = dataTransformer(context.data); return element; } - // handle recursive widget def + // Case 2: For string props, try evaluate it with Expression/LG engine + if (typeof rawPropValue === 'string') { + try { + return evaluateWidgetProp(rawPropValue, { [ActionContextKey]: context.data }, ActionContextKey); + } catch (err) { + return rawPropValue; + } + } + + // Case 3: Recursive widget definition if (typeof rawPropValue === 'object' && rawPropValue.widget) { const widgetSchema = rawPropValue as FlowWidget; return renderUIWidget(widgetSchema, widgetMap, context); diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionCard/ActionCard.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionCard/ActionCard.tsx index 013dc179d2..096711dea4 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionCard/ActionCard.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionCard/ActionCard.tsx @@ -2,8 +2,8 @@ // Licensed under the MIT License. import React, { ReactNode } from 'react'; +import { WidgetContainerProps, WidgetComponent } from '@bfc/extension-client'; -import { WidgetContainerProps, WidgetComponent } from '../../types/flowRenderer.types'; import { ActionHeader } from '../ActionHeader'; import { CardTemplate } from './CardTemplate'; @@ -12,16 +12,37 @@ export interface ActionCardProps extends WidgetContainerProps { header?: ReactNode; body?: ReactNode; footer?: ReactNode; + hideFooter?: boolean; } -export const ActionCard: WidgetComponent = ({ header, body, footer, ...widgetContext }) => { +const safeRender = (input: object | React.ReactNode) => { + if (React.isValidElement(input)) return input; + + // null value is not Valid React element + if (input === null) return null; + + if (typeof input === 'object') { + try { + return JSON.stringify(input); + } catch (err) { + // In case 'input' has circular reference / prototype funcs. + return ''; + } + } + + return input; +}; + +export const ActionCard: WidgetComponent = ({ + header, + body, + footer, + hideFooter = false, + ...widgetContext +}) => { const disabled = widgetContext.data.disabled === true; - return ( - } - /> - ); + const headerNode = safeRender(header) || ; + const bodyNode = safeRender(body); + const footerNode = hideFooter ? null : safeRender(footer); + return ; }; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionHeader/ActionHeader.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionHeader/ActionHeader.tsx index 43194e4580..1fdeda32e0 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionHeader/ActionHeader.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ActionHeader/ActionHeader.tsx @@ -5,8 +5,8 @@ import { useContext } from 'react'; import { jsx } from '@emotion/core'; import { generateSDKTitle } from '@bfc/shared'; +import { WidgetComponent, WidgetContainerProps } from '@bfc/extension-client'; -import { WidgetComponent, WidgetContainerProps } from '../../types/flowRenderer.types'; import { DefaultColors } from '../../constants/ElementColors'; import { RendererContext } from '../../contexts/RendererContext'; import { DisabledIconColor } from '../styles/DisabledStyle'; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/DialogRef.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/DialogRef.tsx index ea0c8a52a1..4e0f3eb9f3 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/DialogRef.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/DialogRef.tsx @@ -4,20 +4,20 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import get from 'lodash/get'; -import { LinkBtn } from '@bfc/ui-shared'; +import { LinkBtn, FixedInfo } from '@bfc/ui-shared'; import { useContext } from 'react'; +import formatMessage from 'format-message'; +import { WidgetContainerProps, WidgetComponent } from '@bfc/extension-client'; -import { WidgetContainerProps, WidgetComponent } from '../types/flowRenderer.types'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { RendererContext } from '../contexts/RendererContext'; import { ElementWrapperTag } from '../types/PluggableComponents.types'; export interface DialogRefCardProps extends WidgetContainerProps { dialog: string | object; - getRefContent?: (dialogRef: JSX.Element | null) => JSX.Element; } -export const DialogRef: WidgetComponent = ({ id, onEvent, dialog, getRefContent }) => { +export const DialogRef: WidgetComponent = ({ id, onEvent, dialog }) => { const { ElementWrapper } = useContext(RendererContext); const calleeDialog = typeof dialog === 'object' ? get(dialog, '$ref') : dialog; const dialogRef = calleeDialog ? ( @@ -31,6 +31,12 @@ export const DialogRef: WidgetComponent = ({ id, onEvent, di {calleeDialog} - ) : null; - return typeof getRefContent === 'function' ? getRefContent(dialogRef) : dialogRef; + ) : ( + '?' + ); + return ( +
+ {dialogRef} {formatMessage('(Dialog)')} +
+ ); }; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ForeachWidget.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ForeachWidget.tsx index 2d1f4c305f..5f236ed385 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ForeachWidget.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ForeachWidget.tsx @@ -4,8 +4,8 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { useMemo, FunctionComponent, useContext } from 'react'; +import { WidgetContainerProps } from '@bfc/extension-client'; -import { WidgetContainerProps } from '../types/flowRenderer.types'; import { transformForeach } from '../transformers/transformForeach'; import { foreachLayouter } from '../layouters/foreachLayouter'; import { GraphNode } from '../models/GraphNode'; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/IfConditionWidget.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/IfConditionWidget.tsx index af776d8698..868f8b3bd4 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/IfConditionWidget.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/IfConditionWidget.tsx @@ -4,8 +4,8 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FunctionComponent, useMemo, useContext } from 'react'; +import { WidgetContainerProps } from '@bfc/extension-client'; -import { WidgetContainerProps } from '../types/flowRenderer.types'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { ifElseLayouter } from '../layouters/ifelseLayouter'; import { NodeEventTypes } from '../constants/NodeEventTypes'; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PromptWidget.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PromptWidget.tsx index a8aa146fa4..960d115fb1 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PromptWidget.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PromptWidget.tsx @@ -5,8 +5,8 @@ import { jsx } from '@emotion/core'; import { FC, useMemo, useContext } from 'react'; import { PromptTab } from '@bfc/shared'; +import { WidgetContainerProps } from '@bfc/extension-client'; -import { WidgetContainerProps } from '../types/flowRenderer.types'; import { baseInputLayouter } from '../layouters/baseInputLayouter'; import { transformBaseInput } from '../transformers/transformBaseInput'; import { GraphNode } from '../models/GraphNode'; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PropertyDescription.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PropertyDescription.tsx new file mode 100644 index 0000000000..2a48c7f4d4 --- /dev/null +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/PropertyDescription.tsx @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { WidgetContainerProps, WidgetComponent } from '@bfc/extension-client'; +import { FixedInfo } from '@bfc/ui-shared'; + +export interface PropertyDescriptionProps extends WidgetContainerProps { + property: any; + description: string; + propertyPlaceholder?: string; + descriptionPlaceholder?: string; +} + +export const PropertyDescription: WidgetComponent = ({ + property, + description, + propertyPlaceholder = '?', + descriptionPlaceholder = '?', +}) => { + return ( + <> + {property || propertyPlaceholder} {description || descriptionPlaceholder} + + ); +}; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ResourceOperation.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ResourceOperation.tsx new file mode 100644 index 0000000000..1ccacc21a0 --- /dev/null +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/ResourceOperation.tsx @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { WidgetContainerProps, WidgetComponent } from '@bfc/extension-client'; +import { FixedInfo, SingleLineDiv } from '@bfc/ui-shared'; + +export interface ResourceOperationProps extends WidgetContainerProps { + operation: string; + resource: string; + singleline?: boolean; +} + +export const ResourceOperation: WidgetComponent = ({ + operation, + resource, + singleline = false, +}) => { + if (singleline) { + return ( + + {operation} {resource} + + ); + } + return ( + <> + {operation} {resource} + + ); +}; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/SwitchConditionWidget.tsx b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/SwitchConditionWidget.tsx index 3fbef0cc18..042da09592 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/SwitchConditionWidget.tsx @@ -4,8 +4,8 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FunctionComponent, useMemo, useContext } from 'react'; +import { WidgetContainerProps } from '@bfc/extension-client'; -import { WidgetContainerProps } from '../types/flowRenderer.types'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; import { switchCaseLayouter } from '../layouters/switchCaseLayouter'; diff --git a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/index.ts b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/index.ts index fb294056ea..825ea4bd71 100644 --- a/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/index.ts +++ b/Composer/packages/adaptive-flow/src/adaptive-flow-renderer/widgets/index.ts @@ -9,3 +9,5 @@ export { SwitchConditionWidget } from './SwitchConditionWidget'; export { ForeachWidget } from './ForeachWidget'; export { ActionHeader } from './ActionHeader'; export { ActionGroup } from './ActionGroup'; +export { PropertyDescription } from './PropertyDescription'; +export { ResourceOperation } from './ResourceOperation'; diff --git a/Composer/packages/extension-client/src/types/extension.ts b/Composer/packages/extension-client/src/types/extension.ts index d17d56df10..fe1a047371 100644 --- a/Composer/packages/extension-client/src/types/extension.ts +++ b/Composer/packages/extension-client/src/types/extension.ts @@ -17,7 +17,7 @@ export type UISchema = { [key in SDKKinds]?: { flow?: FlowWidget; form?: UIOptions; - menu?: MenuOptions; + menu?: MenuOptions | MenuOptions[]; recognizer?: RecognizerOptions; }; }; diff --git a/Composer/packages/extension-client/src/types/flowSchema.ts b/Composer/packages/extension-client/src/types/flowSchema.ts index ff64bad5c4..44560009eb 100644 --- a/Composer/packages/extension-client/src/types/flowSchema.ts +++ b/Composer/packages/extension-client/src/types/flowSchema.ts @@ -36,5 +36,5 @@ type Value = string | number | boolean | undefined | { [key: string]: any }; type PropGenerator = (data: any) => string | number | object | JSX.Element; export type FlowUISchema = { - [key in SDKKinds]?: FlowWidget; + [key in SDKKinds | FlowSchemaBuiltinKeys]?: FlowWidget; }; diff --git a/Composer/packages/lib/ui-shared/src/components/ListOverview.tsx b/Composer/packages/lib/ui-shared/src/components/ListOverview.tsx index 4a048dd71c..cb67d32cbc 100644 --- a/Composer/packages/lib/ui-shared/src/components/ListOverview.tsx +++ b/Composer/packages/lib/ui-shared/src/components/ListOverview.tsx @@ -9,16 +9,18 @@ import { SingleLineDiv } from '../styled/styledComponents'; export interface ListOverviewProps { items: T[]; - renderItem: (item: T) => JSX.Element; + renderItem?: (item: T) => JSX.Element; maxCount?: number; - itemPadding?: number; + itemInterval?: number; + itemSingleline?: boolean; } export const ListOverview: FC> = ({ items, renderItem, maxCount = 3, - itemPadding: itemInterval = 4, + itemInterval = 8, + itemSingleline = true, }) => { if (!Array.isArray(items) || !items.length) { return null; @@ -31,9 +33,16 @@ export const ListOverview: FC> = ({ `} > {items.slice(0, maxCount).map((item, index) => { + let content = item; + if (typeof renderItem === 'function') { + content = renderItem(item); + } else if (typeof item !== 'string') { + content = JSON.stringify(item); + } + return (
- {renderItem(item)} + {itemSingleline ? {content} : content}
); })} diff --git a/Composer/packages/ui-plugins/composer/src/defaultFlowSchema.ts b/Composer/packages/ui-plugins/composer/src/defaultFlowSchema.ts new file mode 100644 index 0000000000..29bec33b61 --- /dev/null +++ b/Composer/packages/ui-plugins/composer/src/defaultFlowSchema.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@bfc/shared'; +import { FlowUISchema } from '@bfc/extension-client'; + +export const DefaultFlowSchema: FlowUISchema = { + [SDKKinds.SendActivity]: { + widget: 'ActionCard', + body: '=action.activity', + }, + [SDKKinds.AttachmentInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.ConfirmInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.DateTimeInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.NumberInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.TextInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.ChoiceInput]: { + widget: 'ActionCard', + body: '=action.prompt', + }, + [SDKKinds.BeginDialog]: { + widget: 'ActionCard', + body: { + widget: 'DialogRef', + dialog: '=action.dialog', + }, + footer: { + widget: 'PropertyDescription', + property: '=action.resultProperty', + description: '= Return value', + }, + hideFooter: '=!action.resultProperty', + }, + [SDKKinds.BeginSkill]: { + widget: 'ActionCard', + colors: { theme: '#004578', color: '#FFFFFF', icon: '#FFFFFF' }, + icon: 'Library', + body: { + widget: 'ResourceOperation', + operation: 'Host', + resource: '=coalesce(action.skillEndpoint, "?")', + singleline: true, + }, + footer: { + widget: 'PropertyDescription', + property: '=action.resultProperty', + description: '= Result', + }, + hideFooter: '=!action.resultProperty', + }, + [SDKKinds.ReplaceDialog]: { + widget: 'ActionCard', + body: { + widget: 'DialogRef', + dialog: '=action.dialog', + }, + }, + [SDKKinds.EditArray]: { + widget: 'ActionCard', + body: { + widget: 'ResourceOperation', + operation: '=coalesce(action.changeType, "?")', + resource: '=coalesce(action.itemsProperty, "?")', + }, + footer: { + widget: 'PropertyDescription', + property: '=action.resultProperty', + description: '= Result', + }, + hideFooter: '=!action.resultProperty', + }, + [SDKKinds.SetProperty]: { + widget: 'ActionCard', + body: '${coalesce(action.property, "?")} : ${coalesce(action.value, "?")}', + }, + [SDKKinds.SetProperties]: { + widget: 'ActionCard', + body: { + widget: 'ListOverview', + items: '=foreach(action.assignments, x => concat(coalesce(x.property, "?"), " : ", coalesce(x.value, "?")))', + }, + }, + [SDKKinds.DeleteProperty]: { + widget: 'ActionCard', + body: '=action.property', + }, + [SDKKinds.DeleteProperties]: { + widget: 'ActionCard', + body: { + widget: 'ListOverview', + items: '=action.properties', + }, + }, + [SDKKinds.DeleteActivity]: { + widget: 'ActionCard', + header: { + widget: 'ActionHeader', + title: 'Delete activity', + }, + body: { + widget: 'PropertyDescription', + property: '=coalesce(action.activityId, "?")', + description: '= ActivityId', + }, + }, + [SDKKinds.UpdateActivity]: { + widget: 'ActionCard', + header: { + widget: 'ActionHeader', + title: 'Update activity', + }, + body: '=action.activity', + }, + [SDKKinds.CancelAllDialogs]: { + widget: 'ActionCard', + body: { + widget: 'PropertyDescription', + property: '=coalesce(action.eventName, "?")', + description: '(Event)', + }, + }, + [SDKKinds.EmitEvent]: { + widget: 'ActionCard', + body: { + widget: 'PropertyDescription', + property: '=coalesce(action.eventName, "?")', + description: '(Event)', + }, + }, + [SDKKinds.HttpRequest]: { + widget: 'ActionCard', + body: { + widget: 'ResourceOperation', + operation: '=action.method', + resource: '=action.url', + singleline: true, + }, + footer: { + widget: 'PropertyDescription', + property: '=action.resultProperty', + description: '= Result property', + }, + hideFooter: '=!action.resultProperty', + }, + [SDKKinds.EditActions]: { + widget: 'ActionCard', + body: '=action.changeType', + }, + [SDKKinds.QnAMakerDialog]: { + widget: 'ActionCard', + body: '=action.hostname', + }, + [SDKKinds.OAuthInput]: { + widget: 'ActionCard', + body: { + widget: 'ResourceOperation', + operation: 'Connection', + resource: '=coalesce(action.connectionName, "?")', + singleline: true, + }, + footer: { + widget: 'PropertyDescription', + property: '=action.property', + description: '= Token property', + }, + hideFooter: '=!action.property', + }, + [SDKKinds.TelemetryTrackEvent]: { + widget: 'ActionCard', + header: { + widget: 'ActionHeader', + title: 'Telemetry - Trace Event', + }, + body: { + widget: 'PropertyDescription', + property: '=coalesce(action.eventName, "?")', + description: '(Event)', + }, + }, +}; diff --git a/Composer/packages/ui-plugins/composer/src/index.ts b/Composer/packages/ui-plugins/composer/src/index.ts index 9fa439562e..6e2f125699 100644 --- a/Composer/packages/ui-plugins/composer/src/index.ts +++ b/Composer/packages/ui-plugins/composer/src/index.ts @@ -1,13 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { PluginConfig, FormUISchema, UISchema, MenuUISchema, RecognizerUISchema } from '@bfc/extension-client'; +import mergeWith from 'lodash/mergeWith'; +import { + PluginConfig, + FormUISchema, + UISchema, + MenuUISchema, + FlowUISchema, + RecognizerUISchema, +} from '@bfc/extension-client'; import { SDKKinds } from '@bfc/shared'; import formatMessage from 'format-message'; -import mergeWith from 'lodash/mergeWith'; import { IntentField, RecognizerField, QnAActionsField } from '@bfc/adaptive-form'; import { DefaultMenuSchema } from './defaultMenuSchema'; +import { DefaultFlowSchema } from './defaultFlowSchema'; import { DefaultRecognizerSchema } from './defaultRecognizerSchema'; const DefaultFormSchema: FormUISchema = { @@ -164,17 +172,19 @@ const DefaultFormSchema: FormUISchema = { const synthesizeUISchema = ( formSchema: FormUISchema, menuSchema: MenuUISchema, + flowSchema: FlowUISchema, recognizerSchema: RecognizerUISchema ): UISchema => { let uischema: UISchema = {}; uischema = mergeWith(uischema, formSchema, (origin, formOption) => ({ ...origin, form: formOption })); uischema = mergeWith(uischema, menuSchema, (origin, menuOption) => ({ ...origin, menu: menuOption })); + uischema = mergeWith(uischema, flowSchema, (origin, flowOption) => ({ ...origin, flow: flowOption })); uischema = mergeWith(uischema, recognizerSchema, (origin, opt) => ({ ...origin, recognizer: opt })); return uischema; }; const config: PluginConfig = { - uiSchema: synthesizeUISchema(DefaultFormSchema, DefaultMenuSchema, DefaultRecognizerSchema), + uiSchema: synthesizeUISchema(DefaultFormSchema, DefaultMenuSchema, DefaultFlowSchema, DefaultRecognizerSchema), }; export default config; diff --git a/Composer/packages/ui-plugins/prompts/src/index.tsx b/Composer/packages/ui-plugins/prompts/src/index.tsx index e12720da09..db3048e914 100644 --- a/Composer/packages/ui-plugins/prompts/src/index.tsx +++ b/Composer/packages/ui-plugins/prompts/src/index.tsx @@ -79,6 +79,7 @@ const PropertyInfo = (data) => const ChoiceInputBody = (data) => Array.isArray(data.choices) && data.choices.length ? ( { const value = typeof item === 'object' ? item.value : item;