Skip to content

Commit

Permalink
feat: Trigger UI Schema (microsoft#4079)
Browse files Browse the repository at this point in the history
* chore: move TriggerCreationModal to folder

* chore: move styles into styles.ts

* chore: move validators out

* Extract dropdown options as constants

* remove outdated dropdown option generators

* don't re-declare SDKKinds as type key

* remove initialError, fix tslint

* fix warning icon size

* refactor show warning logic

* move static func outside TriggerModal

* refactor: move trigger widget out of Modal

* replay changes in microsoft#4117 'mutiple projects'

* fix a wrong import path

* refactor: extract TriggerDropdownGroup

* define builtinSchema

* TriggerOptionTree

* remove duplicated  $kinds

* link leaf ndoe to parent node

* migrate to option tree

* fix a React grammar

* rename builtinSchema

* refactor the warning icon logic of trigger modal

* remove unreferenced utils

* add a todo

* fix UT by adding data-testid

* declare TriggerUISchema in extension

* use trigger uischema from extension context

* check trigger option existence

* sort trigger dropdown labels

* move root text out of tree utils

* pass in option compare fn

* add UT for triggerOptionTree

* align icon size with main

* migrate 1.2 PVA logic

* use 'Boolean' to filter trigger menus

Co-authored-by: Andy Brown <asbrown002@gmail.com>

* wrap trigger UI Schema with formatMessage

* add 'px' unit to styles

* add trigger menu order

* 'order' property to manage trigger order

* early returning & add comments

* avoid duplicated iteration and add comments

* fix trigger modal UT

* lint

* replay Ben's commit

* CI fix

Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com>
Co-authored-by: Andy Brown <asbrown002@gmail.com>
  • Loading branch information
3 people authored Jan 27, 2021
1 parent 69d64c2 commit 92e68a5
Show file tree
Hide file tree
Showing 25 changed files with 981 additions and 638 deletions.
4 changes: 2 additions & 2 deletions Composer/cypress/integration/TriggerCreation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ context('Creating a new trigger', () => {
cy.findAllByText('Add a trigger').click({ force: true });
cy.findByTestId('triggerTypeDropDown').click();
cy.get('[title="Dialog events"]').click();
cy.findByTestId('eventTypeDropDown').click();
cy.findByText('Select an event type').click();
cy.findByText('Dialog started (Begin dialog event)').click();
cy.findByTestId('triggerFormSubmit').click();
cy.findAllByText('Begin dialog event').should('exist');
Expand All @@ -55,7 +55,7 @@ context('Creating a new trigger', () => {
cy.findAllByText('Add a trigger').click({ force: true });
cy.findByTestId('triggerTypeDropDown').click();
cy.get('[title="Activities"]').click();
cy.findByTestId('activityTypeDropDown').click();
cy.findByText('Select an activity type').click();
cy.findByText('Activities (Activity received)').click();
cy.findByTestId('triggerFormSubmit').click();
cy.findAllByText('Activities').should('exist');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,45 @@

import * as React from 'react';
import { fireEvent, waitFor } from '@botframework-composer/test-utils';
import { EditorExtension, PluginConfig } from '@bfc/extension-client';

import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal';
import { renderWithRecoil } from '../testUtils';
import { TriggerCreationModal } from '../../../src/components/TriggerCreationModal';
import { renderWithRecoil } from '../../testUtils';

const projectId = '123a-bv3c4';

describe('<TriggerCreationModal/>', () => {
const onSubmitMock = jest.fn();
const onDismissMock = jest.fn();

const pluginsStub: PluginConfig = {
uiSchema: {
'Microsoft.OnIntent': {
trigger: {
label: 'Intent recognized',
order: 1,
},
},
'Microsoft.OnQnAMatch': {
trigger: {
label: 'QnA Intent recognized',
order: 2,
},
},
},
};

function renderComponent() {
return renderWithRecoil(
<TriggerCreationModal
isOpen
dialogId={'todobot'}
projectId={projectId}
onDismiss={onDismissMock}
onSubmit={onSubmitMock}
/>
<EditorExtension plugins={pluginsStub} projectId={''} shell={{ api: {} as any, data: {} as any }}>
<TriggerCreationModal
isOpen
dialogId={'todobot'}
projectId={projectId}
onDismiss={onDismissMock}
onSubmit={onSubmitMock}
/>
</EditorExtension>
);
}

Expand All @@ -30,7 +50,7 @@ describe('<TriggerCreationModal/>', () => {
expect(component.container).toBeDefined();
});

it('hould create a Luis Intent recognized', async () => {
it('should create a Luis Intent recognized', async () => {
const component = renderComponent();
const triggerType = component.getByTestId('triggerTypeDropDown');
fireEvent.click(triggerType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { TriggerUISchema } from '@bfc/extension-client';
import { SDKKinds } from '@botframework-composer/types';

import {
generateTriggerOptionTree,
TriggerOptionGroupNode,
} from '../../../src/components/TriggerCreationModal/TriggerOptionTree';

describe('generateTriggerOptionTree()', () => {
it('can generate one layer tree.', () => {
const simpleTriggerUIOptions: TriggerUISchema = {
[SDKKinds.OnIntent]: {
label: '1.OnIntent',
order: 1,
},
[SDKKinds.OnInvokeActivity]: {
label: '2.OnInvokeActivity',
order: 2,
},
};
const tree = generateTriggerOptionTree(simpleTriggerUIOptions, 'Select a trigger', 'Which trigger?');

expect(tree.prompt).toEqual('Select a trigger');
expect(tree.placeholder).toEqual('Which trigger?');

expect(tree.parent).toBeNull();
expect(tree.children.length).toEqual(2);

expect(tree.children[0].label).toEqual('1.OnIntent');
expect(tree.children[0].parent).toEqual(tree);

expect(tree.children[1].label).toEqual('2.OnInvokeActivity');
expect(tree.children[1].parent).toEqual(tree);
});

it('can generate tree with submenu.', () => {
const advancedTriggerUIOptions: TriggerUISchema = {
[SDKKinds.OnIntent]: {
label: '1.OnIntent',
order: 1,
},
[SDKKinds.OnTypingActivity]: {
label: '2.1.OnTypingActivity',
order: 2.1,
submenu: {
label: '2.Activities',
prompt: 'Select an activity trigger',
placeholder: 'Which activity?',
},
},
[SDKKinds.OnEventActivity]: {
label: '2.2OnEventActivity',
order: 2.2,
submenu: '2.Activities',
},
[SDKKinds.OnInvokeActivity]: {
label: '2.3OnInvokeActivity',
order: 2.3,
submenu: '2.Activities',
},
};
const tree = generateTriggerOptionTree(advancedTriggerUIOptions, 'Select a trigger', 'Which trigger?');

expect(tree.children.length).toEqual(2);

expect(tree.children[0].label).toEqual('1.OnIntent');
expect(tree.children[0].parent).toEqual(tree);

const secondChild = tree.children[1] as TriggerOptionGroupNode;
expect(secondChild.label).toEqual('2.Activities');
expect(secondChild.prompt).toEqual('Select an activity trigger');
expect(secondChild.children.length).toEqual(3);

expect(secondChild.children[0].label).toEqual('2.1.OnTypingActivity');
expect(secondChild.children[0].parent).toEqual(secondChild);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fireEvent } from '@botframework-composer/test-utils';
import { renderWithRecoil } from '../testUtils';
import { SAMPLE_DIALOG } from '../mocks/sampleDialog';
import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree';
import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal';
import { TriggerCreationModal } from '../../src/components/TriggerCreationModal';
import { CreateDialogModal } from '../../src/pages/design/createDialogModal';
import {
dialogsSelectorFamily,
Expand Down
49 changes: 0 additions & 49 deletions Composer/packages/client/__tests__/utils/dialogUtil.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import {
updateRegExIntent,
createSelectedPath,
deleteTrigger,
getTriggerTypes,
getEventTypes,
getActivityTypes,
getFriendlyName,
getBreadcrumbLabel,
getSelected,
Expand Down Expand Up @@ -181,52 +178,6 @@ describe('deleteTrigger', () => {
});
});

describe('getTriggerTypes', () => {
it('return trigger types', () => {
const triggerTypes = getTriggerTypes();
expect(triggerTypes).toEqual([
{ key: 'Microsoft.OnIntent', text: 'Intent recognized' },
{ key: 'Microsoft.OnQnAMatch', text: 'QnA Intent recognized' },
{ key: 'Microsoft.OnUnknownIntent', text: 'Unknown intent' },
{ key: 'Microsoft.OnDialogEvent', text: 'Dialog events' },
{ key: 'Microsoft.OnActivity', text: 'Activities' },
{ key: 'Microsoft.OnChooseIntent', text: 'Duplicated intents recognized' },
{ key: 'OnCustomEvent', text: 'Custom events' },
]);
});
});

describe('getEventTypes', () => {
it('return event types', () => {
const eventTypes = getEventTypes();
expect(eventTypes).toEqual([
{ key: 'Microsoft.OnBeginDialog', text: 'Dialog started (Begin dialog event)' },
{ key: 'Microsoft.OnCancelDialog', text: 'Dialog cancelled (Cancel dialog event)' },
{ key: 'Microsoft.OnError', text: 'Error occurred (Error event)' },
{ key: 'Microsoft.OnRepromptDialog', text: 'Re-prompt for input (Reprompt dialog event)' },
]);
});
});

describe('getActivityTypes', () => {
it('return activity types', () => {
const activityTypes = getActivityTypes();
expect(activityTypes).toEqual([
{ key: 'Microsoft.OnActivity', text: 'Activities (Activity received)' },
{ key: 'Microsoft.OnConversationUpdateActivity', text: 'Greeting (ConversationUpdate activity)' },
{ key: 'Microsoft.OnEndOfConversationActivity', text: 'Conversation ended (EndOfConversation activity)' },
{ key: 'Microsoft.OnEventActivity', text: 'Event received (Event activity)' },
{ key: 'Microsoft.OnHandoffActivity', text: 'Handover to human (Handoff activity)' },
{ key: 'Microsoft.OnInvokeActivity', text: 'Conversation invoked (Invoke activity)' },
{ key: 'Microsoft.OnTypingActivity', text: 'User is typing (Typing activity)' },
{ key: 'Microsoft.OnMessageActivity', text: 'Message received (Message received activity)' },
{ key: 'Microsoft.OnMessageDeleteActivity', text: 'Message deleted (Message deleted activity)' },
{ key: 'Microsoft.OnMessageReactionActivity', text: 'Message reaction (Message reaction activity)' },
{ key: 'Microsoft.OnMessageUpdateActivity', text: 'Message updated (Message updated activity)' },
]);
});
});

describe('getFriendlyName', () => {
it('return friendly name', () => {
const name = getFriendlyName(dialogs[0].content);
Expand Down
20 changes: 9 additions & 11 deletions Composer/packages/client/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ const dotenvFiles = [
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv
paths.dotenv,
].filter(Boolean);

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
dotenvFiles.forEach((dotenvFile) => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile
path: dotenvFile,
})
);
}
Expand All @@ -61,8 +61,8 @@ function getGitSha() {
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.filter((folder) => folder && !path.isAbsolute(folder))
.map((folder) => path.resolve(appDirectory, folder))
.join(path.delimiter);

// Grab NODE_ENV and COMPOSER_* environment variables and prepare them to be
Expand All @@ -71,7 +71,7 @@ const COMPOSER = /^COMPOSER_/i;

function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => COMPOSER.test(key))
.filter((key) => COMPOSER.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
Expand All @@ -86,24 +86,22 @@ function getClientEnvironment(publicUrl) {
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
GIT_SHA: getGitSha()
.toString()
.replace('\n', ''),
GIT_SHA: getGitSha().toString().replace('\n', ''),
SDK_PACKAGE_VERSION: '4.11.0', // TODO: change this when Composer supports custom schema/custom runtime
COMPOSER_VERSION: '1.3.1',
LOCAL_PUBLISH_PATH:
process.env.LOCAL_PUBLISH_PATH || path.resolve(process.cwd(), '../../../extensions/localPublish/hostedBots'),
WEBLOGIN_CLIENTID: process.env.WEBLOGIN_CLIENTID,
WEBLOGIN_TENANTID: process.env.WEBLOGIN_TENANTID,
WEBLOGIN_REDIRECTURL: process.env.WEBLOGIN_REDIRECTURL
WEBLOGIN_REDIRECTURL: process.env.WEBLOGIN_REDIRECTURL,
}
);
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {})
}, {}),
};

return { raw, stringified };
Expand Down
Loading

0 comments on commit 92e68a5

Please sign in to comment.