diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 37b101e51f..bd250e2ee8 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -593,6 +593,9 @@ "close_d634289d": { "message": "Close" }, + "code_editor_1438efc": { + "message": "code editor" + }, "cognitive_service_region_87c668be": { "message": "Cognitive Service Region" }, @@ -698,6 +701,9 @@ "connect_to_a_skill_53c9dff0": { "message": "Connect to a skill" }, + "connect_to_an_existing_bot_7f42990b": { + "message": "Connect to an existing bot" + }, "connect_to_qna_knowledgebase_4b324132": { "message": "Connect to QnA Knowledgebase" }, @@ -776,6 +782,9 @@ "create_a_name_for_the_project_which_will_be_used_t_57e9b690": { "message": "Create a name for the project which will be used to name the application: (projectname-environment-LUfilename)" }, + "create_a_new_bot_51ce70d3": { + "message": "Create a new bot" + }, "create_a_new_dialog_21d84b82": { "message": "Create a new dialog" }, @@ -1079,6 +1088,9 @@ "display_text_used_by_the_channel_to_render_visuall_4e4ab704": { "message": "Display text used by the channel to render visually." }, + "do_you_want_to_create_a_new_bot_or_connect_your_az_f5c9dee": { + "message": "Do you want to create a new bot, or connect your Azure Bot resource to an existing bot?" + }, "do_you_want_to_proceed_cd35aa38": { "message": "Do you want to proceed?" }, @@ -1484,6 +1496,9 @@ "for_properties_of_type_list_or_enum_your_bot_accep_9e7649c6": { "message": "For properties of type list (or enum), your bot accepts only the values you define. After your dialog is generated, you can provide synonyms for each value." }, + "form_b674666c": { + "message": "form" + }, "form_dialog_error_ba7c37fe": { "message": "Form dialog error" }, @@ -2327,6 +2342,9 @@ "one_of_the_variations_added_below_will_be_selected_bee3c3f1": { "message": "One of the variations added below will be selected at random by the LG library." }, + "one_or_more_options_that_are_passed_to_the_dialog__cbcf5d72": { + "message": "One or more options that are passed to the dialog that is called." + }, "open_an_existing_skill_fbd87273": { "message": "Open an existing skill" }, @@ -2348,6 +2366,9 @@ "open_web_chat_7a24d4f8": { "message": "Open web chat" }, + "open_your_azure_bot_resource_9165e3d1": { + "message": "Open your Azure Bot resource" + }, "optional_221bcc9d": { "message": "Optional" }, diff --git a/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx b/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx new file mode 100644 index 0000000000..e5fd0575c9 --- /dev/null +++ b/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import styled from '@emotion/styled'; +import { FieldProps, JSONSchema7, useShellApi } from '@bfc/extension-client'; +import { FieldLabel, JsonField, SchemaField, IntellisenseTextField, WithTypeIcons } from '@bfc/adaptive-form'; +import Stack from 'office-ui-fabric-react/lib/components/Stack/Stack'; +import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; + +const IntellisenseTextFieldWithIcon = WithTypeIcons(IntellisenseTextField); + +const BorderedStack = styled(Stack)(({ border }: { border: boolean }) => + border + ? { + borderBottom: `1px solid ${NeutralColors.gray30}`, + } + : {} +); + +const styles = { + dropdown: { + root: { + ':hover .ms-Dropdown-title, :active .ms-Dropdown-title, :hover .ms-Dropdown-caretDown, :active .ms-Dropdown-caretDown': { + color: FluentTheme.palette.themeDarker, + }, + ':focus-within .ms-Dropdown-title, :focus-within .ms-Dropdown-caretDown': { + color: FluentTheme.palette.accent, + }, + }, + caretDown: { fontSize: FluentTheme.fonts.xSmall.fontSize, color: FluentTheme.palette.accent }, + dropdownOptionText: { fontSize: FluentTheme.fonts.small.fontSize }, + title: { + border: 'none', + fontSize: FluentTheme.fonts.small.fontSize, + color: FluentTheme.palette.accent, + }, + }, +}; + +const dropdownCalloutProps = { styles: { root: { minWidth: 140 } } }; + +const getInitialSelectedKey = (value?: string | Record, schema?: JSONSchema7): string => { + if (typeof value !== 'string' && schema) { + return 'form'; + } else if (typeof value !== 'string' && !schema) { + return 'code'; + } else { + return 'expression'; + } +}; + +const DialogOptionsField: React.FC = ({ + description, + uiOptions, + label, + required, + id, + value = {}, + onChange, +}) => { + const { dialog, options } = value; + const { dialogSchemas } = useShellApi(); + const { content: schema }: { content?: JSONSchema7 } = React.useMemo( + () => dialogSchemas.find(({ id }) => id === dialog) || {}, + [dialog, dialogSchemas] + ); + + const [selectedKey, setSelectedKey] = React.useState(getInitialSelectedKey(options, schema)); + + React.useLayoutEffect(() => { + setSelectedKey(getInitialSelectedKey(options, schema)); + }, [dialog]); + + const change = React.useCallback( + (newOptions?: string | Record) => { + onChange({ ...value, options: newOptions }); + }, + [value, onChange] + ); + + const onDropdownChange = React.useCallback( + (_: React.FormEvent, option?: IDropdownOption) => { + if (option) { + setSelectedKey(option.key as string); + if (option.key === 'expression') { + change(); + } + } + }, + [change] + ); + + const typeOptions = React.useMemo(() => { + return [ + { + key: 'form', + text: formatMessage('form'), + disabled: !schema || !Object.keys(schema).length, + }, + { + key: 'code', + text: formatMessage('code editor'), + }, + { + key: 'expression', + text: 'expression', + }, + ]; + }, [schema]); + + let Field = IntellisenseTextFieldWithIcon; + if (selectedKey === 'form') { + Field = SchemaField; + } else if (selectedKey === 'code') { + Field = JsonField; + } + + return ( + + + + + + + + ); +}; + +export { DialogOptionsField }; diff --git a/Composer/packages/ui-plugins/select-dialog/src/__tests__/DialogOptions.test.tsx b/Composer/packages/ui-plugins/select-dialog/src/__tests__/DialogOptions.test.tsx new file mode 100644 index 0000000000..9c213347d4 --- /dev/null +++ b/Composer/packages/ui-plugins/select-dialog/src/__tests__/DialogOptions.test.tsx @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @ts-nocheck + +import React from 'react'; +import { fireEvent, render } from '@botframework-composer/test-utils'; +import { EditorExtension } from '@bfc/extension-client'; + +import { DialogOptionsField } from '../DialogOptionsField'; + +jest.mock('@bfc/adaptive-form', () => { + const AdaptiveForm = jest.requireActual('@bfc/adaptive-form'); + + return { + ...AdaptiveForm, + JsonField: () =>
Json Field
, + SchemaField: () =>
Options Form
, + IntellisenseTextField: () =>
Intellisense Text Field
, + }; +}); + +jest.mock('office-ui-fabric-react/lib/Dropdown', () => { + const Dropdown = jest.requireActual('office-ui-fabric-react/lib/Dropdown'); + + return { + ...Dropdown, + Dropdown: ({ onChange }) => ( + + ), + }; +}); + +const renderDialogOptionsField = ({ value } = {}) => { + const props = { + description: 'Options passed to the dialog.', + id: 'dialog.options', + label: 'Dialog options', + value, + }; + + const shell = {}; + + const shellData = { + dialogs: [ + { id: 'dialog1', displayName: 'dialog1' }, + { id: 'dialog2', displayName: 'dialog2' }, + { id: 'dialog3', displayName: 'dialog3' }, + ], + dialogSchemas: [ + { + id: 'dialog1', + content: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + bar: { + type: 'number', + }, + }, + }, + }, + ], + }; + return render( + + + + ); +}; + +describe('DialogOptionsField', () => { + it('should render label', async () => { + const { findByText } = renderDialogOptionsField(); + await findByText('Dialog options'); + }); + it('should render the options form if the dialog schema is defined and options is not a string', async () => { + const { findByText } = renderDialogOptionsField({ value: { dialog: 'dialog1', options: {} } }); + await findByText('Options Form'); + }); + it('should render the JsonField if the dialog schema is undefined and options is not a string', async () => { + const { findByText } = renderDialogOptionsField({ value: { dialog: 'dialog2', options: {} } }); + await findByText('Json Field'); + }); + it('should render the IntellisenseTextField if options is a string', async () => { + const { findByText } = renderDialogOptionsField({ value: { dialog: 'dialog2', options: '=user.data' } }); + await findByText('Intellisense Text Field'); + }); + it('should be able to switch between fields', async () => { + const { findByText } = renderDialogOptionsField({ + value: { dialog: 'dialog1', options: {} }, + }); + + // Should initially render Options Form + await findByText('Options Form'); + + // Switch to Json field + const button = await findByText('Switch to Json Field'); + fireEvent.click(button); + + await findByText('Json Field'); + }); +}); diff --git a/Composer/packages/ui-plugins/select-dialog/src/index.ts b/Composer/packages/ui-plugins/select-dialog/src/index.ts index ad19ba9ed8..82842c016f 100644 --- a/Composer/packages/ui-plugins/select-dialog/src/index.ts +++ b/Composer/packages/ui-plugins/select-dialog/src/index.ts @@ -3,26 +3,42 @@ import { PluginConfig } from '@bfc/extension-client'; import { SDKKinds } from '@bfc/shared'; +import formatMessage from 'format-message'; import { SelectDialog } from './SelectDialog'; +import { DialogOptionsField } from './DialogOptionsField'; const config: PluginConfig = { uiSchema: { [SDKKinds.BeginDialog]: { form: { + hidden: ['options'], properties: { dialog: { field: SelectDialog, }, + dialogOptions: { + additionalField: true, + field: DialogOptionsField, + label: () => formatMessage('Options'), + description: () => formatMessage('One or more options that are passed to the dialog that is called.'), + }, }, }, }, [SDKKinds.ReplaceDialog]: { form: { + hidden: ['options'], properties: { dialog: { field: SelectDialog, }, + dialogOptions: { + additionalField: true, + field: DialogOptionsField, + label: () => formatMessage('Options'), + description: () => formatMessage('One or more options that are passed to the dialog that is called.'), + }, }, }, },