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

feat: Add begin dialog options form #6508

Merged
merged 5 commits into from
Mar 24, 2021
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
21 changes: 21 additions & 0 deletions Composer/packages/server/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@
"close_d634289d": {
"message": "Close"
},
"code_editor_1438efc": {
"message": "code editor"
},
"cognitive_service_region_87c668be": {
"message": "Cognitive Service Region"
},
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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?"
},
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
},
Expand All @@ -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"
},
Expand Down
156 changes: 156 additions & 0 deletions Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, 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<FieldProps> = ({
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<string>(getInitialSelectedKey(options, schema));

React.useLayoutEffect(() => {
setSelectedKey(getInitialSelectedKey(options, schema));
}, [dialog]);

const change = React.useCallback(
(newOptions?: string | Record<string, any>) => {
onChange({ ...value, options: newOptions });
},
[value, onChange]
);

const onDropdownChange = React.useCallback(
(_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => {
if (option) {
setSelectedKey(option.key as string);
if (option.key === 'expression') {
change();
}
}
},
[change]
);

const typeOptions = React.useMemo<IDropdownOption[]>(() => {
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 (
<React.Fragment>
<BorderedStack horizontal border={selectedKey === 'form'} horizontalAlign={'space-between'}>
<FieldLabel
description={description}
helpLink={uiOptions?.helpLink}
id={id}
label={label}
required={required}
/>
<Dropdown
ariaLabel={formatMessage('Select property type')}
calloutProps={dropdownCalloutProps}
dropdownWidth={-1}
options={typeOptions}
selectedKey={selectedKey}
styles={styles.dropdown}
onChange={onDropdownChange}
/>
</BorderedStack>
<Field
definitions={schema?.definitions || {}}
depth={-1}
id={`${id}.options`}
label={false}
name={'options'}
schema={schema || {}}
uiOptions={{}}
value={options || selectedKey === 'expression' ? '' : {}}
onChange={change}
/>
</React.Fragment>
);
};

export { DialogOptionsField };
Original file line number Diff line number Diff line change
@@ -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: () => <div>Json Field</div>,
SchemaField: () => <div>Options Form</div>,
IntellisenseTextField: () => <div>Intellisense Text Field</div>,
};
});

jest.mock('office-ui-fabric-react/lib/Dropdown', () => {
const Dropdown = jest.requireActual('office-ui-fabric-react/lib/Dropdown');

return {
...Dropdown,
Dropdown: ({ onChange }) => (
<button
onClick={(e) => {
onChange(e, { key: 'code' });
}}
>
Switch to Json Field
</button>
),
};
});

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(
<EditorExtension shell={{ api: shell, data: shellData }}>
<DialogOptionsField {...props} />
</EditorExtension>
);
};

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');
});
});
16 changes: 16 additions & 0 deletions Composer/packages/ui-plugins/select-dialog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
},
},
},
},
Expand Down