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: update new trigger modal according to design #1786

Merged
merged 25 commits into from
Mar 1, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { dialogs } from '../constants.json';
import { TriggerCreationModal } from './../../src/components/ProjectTree/TriggerCreationModal';
import { ProjectTree } from './../../src/components/ProjectTree';
import { CreateDialogModal } from './../../src/pages/design/createDialogModal';

jest.mock('@bfc/code-editor', () => {
return {
LuEditor: () => <div></div>,
};
});

describe('<ProjectTree/>', () => {
it('should render the ProjectTree', async () => {
const dialogId = 'Main';
Expand Down
12 changes: 6 additions & 6 deletions Composer/packages/client/src/ShellApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,19 +217,19 @@ export const ShellApi: React.FC = () => {
if (!file) throw new Error(`lu file ${id} not found`);
if (!intentName) throw new Error(`intentName is missing or empty`);

const newLuContent = luUtil.updateIntent(file.content, intentName, intent);
const content = luUtil.updateIntent(file.content, intentName, intent);

return await updateLuFile({ id, newLuContent });
return await updateLuFile({ id, content });
}

async function addLuIntentHandler({ id, intent }, event) {
if (isEventSourceValid(event) === false) return false;
const file = luFiles.find(file => file.id === id);
if (!file) throw new Error(`lu file ${id} not found`);

const newLuContent = luUtil.addIntent(file.content, intent);
const content = luUtil.addIntent(file.content, intent);

return await updateLuFile({ id, newLuContent });
return await updateLuFile({ id, content });
}

async function removeLuIntentHandler({ id, intentName }, event) {
Expand All @@ -238,9 +238,9 @@ export const ShellApi: React.FC = () => {
if (!file) throw new Error(`lu file ${id} not found`);
if (!intentName) throw new Error(`intentName is missing or empty`);

const newLuContent = luUtil.removeIntent(file.content, intentName);
const content = luUtil.removeIntent(file.content, intentName);

return await updateLuFile({ id, newLuContent });
return await updateLuFile({ id, content });
}

async function fileHandler(fileTargetType, fileChangeType, { id, content }, event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import React, { useState, useContext } from 'react';
import formatMessage from 'format-message';
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { DialogInfo, luIndexer, combineMessage } from '@bfc/indexers';
import get from 'lodash/get';
import { DialogInfo } from '@bfc/indexers';
import { LuEditor } from '@bfc/code-editor';

import {
addNewTrigger,
Expand All @@ -25,15 +28,16 @@ import {
getEventTypes,
getActivityTypes,
getMessageTypes,
regexRecognizerKey,
} from '../../utils/dialogUtil';
import { addIntent } from '../../utils/luUtil';
import { StoreContext } from '../../store';

import { styles, dropdownStyles, dialogWindow } from './styles';
import { styles, dropdownStyles, dialogWindow, intent } from './styles';

const nameRegex = /^[a-zA-Z0-9-_.]+$/;
const validateForm = (data: TriggerFormData): TriggerFormDataErrors => {
const errors: TriggerFormDataErrors = {};
const { $type, specifiedType } = data;
const { $type, specifiedType, intent, triggerPhrases } = data;

if ($type === eventTypeKey && !specifiedType) {
errors.specifiedType = formatMessage('Please select a event type');
Expand All @@ -46,21 +50,40 @@ const validateForm = (data: TriggerFormData): TriggerFormDataErrors => {
if (!$type) {
errors.$type = formatMessage('Please select a trigger type');
}

if (!intent || !nameRegex.test(intent)) {
errors.intent = formatMessage(
'Spaces and special characters are not allowed. Use letters, numbers, -, or _., numbers, -, and _'
);
}

if (!triggerPhrases) {
errors.triggerPhrases = formatMessage('Please input trigger phrases');
}
if (data.errors.triggerPhrases) {
errors.triggerPhrases = data.errors.triggerPhrases;
}
return errors;
};

interface LuFilePayload {
id: string;
content: string;
}

interface TriggerCreationModalProps {
dialogId: string;
isOpen: boolean;
onDismiss: () => void;
onSubmit: (dialog: DialogInfo) => void;
onSubmit: (dialog: DialogInfo, luFilePayload: LuFilePayload) => void;
}

const initialFormData: TriggerFormData = {
errors: {},
$type: intentTypeKey,
intent: '',
specifiedType: '',
intent: '',
triggerPhrases: '',
};

const triggerTypeOptions: IDropdownOption[] = getTriggerTypes();
Expand All @@ -71,7 +94,7 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props =
const { state } = useContext(StoreContext);
const { dialogs, luFiles } = state;
const luFile = luFiles.find(lu => lu.id === dialogId);
const dialogFile = dialogs.find(dialog => dialog.id === dialogId);

const onClickSubmitButton = e => {
e.preventDefault();
const errors = validateForm(formData);
Expand All @@ -83,38 +106,43 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props =
});
return;
}

const content = get(luFile, 'content', '');
const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases });
const updateLuFile = {
id: dialogId,
content: newContent,
};
const newDialog = addNewTrigger(dialogs, dialogId, formData);
onSubmit(newDialog);
onSubmit(newDialog, updateLuFile);
onDismiss();
};

const onSelectTriggerType = (e, option) => {
setFormData({ ...initialFormData, $type: option.key });
};

const onSelectIntent = (e, option) => {
setFormData({ ...formData, intent: option.key });
};

const onSelectSpecifiedTypeType = (e, option) => {
setFormData({ ...formData, specifiedType: option.key });
};

const onNameChange = (e, name) => {
setFormData({ ...formData, intent: name });
};

const onTriggerPhrasesChange = (body: string) => {
const errors = formData.errors;
const content = '#' + formData.intent + '\n' + body;
const { diagnostics } = luIndexer.parse(content);
errors.triggerPhrases = combineMessage(diagnostics);
setFormData({ ...formData, triggerPhrases: body, errors });
};

const eventTypes: IDropdownOption[] = getEventTypes();
const activityTypes: IDropdownOption[] = getActivityTypes();
const messageTypes: IDropdownOption[] = getMessageTypes();

const isRegEx = get(dialogFile, 'content.recognizer.$type', '') === regexRecognizerKey;

const regexIntents = get(dialogFile, 'content.recognizer.intents', []);
const luisIntents = get(luFile, 'intents', []);
const intents = isRegEx ? regexIntents : luisIntents;

const intentOptions = intents.map(t => {
return { key: t.name || t.Name || t.intent, text: t.name || t.Name || t.intent };
});

const showIntentDropDown = formData.$type === intentTypeKey;
const showIntentFields = formData.$type === intentTypeKey;
const showEventDropDown = formData.$type === eventTypeKey;
const showActivityDropDown = formData.$type === activityTypeKey;
const showMessageDropDown = formData.$type === messageTypeKey;
Expand Down Expand Up @@ -178,15 +206,27 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props =
data-testid={'messageTypeDropDown'}
/>
)}
{showIntentDropDown && (
<Dropdown
label={formatMessage('Which intent do you want to handle? (Optional)')}
options={intentOptions}
styles={dropdownStyles}
onChange={onSelectIntent}
disabled={intentOptions.length === 0}
placeholder={intentOptions.length === 0 ? formatMessage('No intents configured for this dialog') : ''}
{showIntentFields && (
<TextField
label={formatMessage('What is the name of this trigger')}
styles={intent}
onChange={onNameChange}
errorMessage={formData.errors.intent}
data-testid="TriggerName"
/>
)}
{showIntentFields && <Label>{formatMessage('Trigger Phrases')}</Label>}
{showIntentFields && (
<LuEditor
onChange={onTriggerPhrasesChange}
value={formData.triggerPhrases}
errorMsg={formData.errors.triggerPhrases}
hidePlaceholder={true}
luOption={{
fileId: dialogId,
sectionId: formData.intent || 'newSection',
}}
height={150}
/>
)}
</Stack>
Expand Down
18 changes: 12 additions & 6 deletions Composer/packages/client/src/components/ProjectTree/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const dropdownStyles = {
fontWeight: FontWeights.semibold,
},
dropdown: {
width: '300px',
width: '400px',
},
root: {
paddingBottom: '20px',
Expand All @@ -148,7 +148,7 @@ export const dialogWindow = css`
display: flex;
flex-direction: column;
width: 400px;
height: 250px;
min-height: 300px;
`;

export const textFieldlabel = {
Expand All @@ -162,11 +162,17 @@ export const textFieldlabel = {
};

export const intent = {
fieldGroup: {
width: 200,
root: {
width: '400px',
paddingBottom: '20px',
},
};

export const triggerPhrases = {
root: {
height: '90px',
width: '400px',
},
fieldGroup: {
height: 80,
},
subComponentStyles: textFieldlabel,
};
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ const shellApi: ShellApi = {
});
},

addLuIntent: (id, intent) => {
return apiClient.apiCall('addLuIntent', { id, intent });
},

updateLuIntent: (id, intentName, intent) => {
return apiClient.apiCall('updateLuIntent', { id, intentName, intent });
},

removeLuIntent: (id, intentName) => {
return apiClient.apiCall('removeLuIntent', { id, intentName });
},

createDialog: () => {
return apiClient.apiCall('createDialog');
},
Expand Down
11 changes: 8 additions & 3 deletions Composer/packages/client/src/pages/design/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,19 @@ function DesignPage(props) {
setTriggerModalVisibility(true);
};

const onTriggerCreationSubmit = dialog => {
const payload = {
const onTriggerCreationSubmit = (dialog, luFile) => {
const dialogPayload = {
id: dialog.id,
content: dialog.content,
};
const luFilePayload = {
id: luFile.id,
content: luFile.content,
};
const index = get(dialog, 'content.triggers', []).length - 1;
actions.selectTo(`triggers[${index}]`);
actions.updateDialog(payload);
actions.updateLuFile(luFilePayload);
actions.updateDialog(dialogPayload);
};

function handleSelect(id, selected = '') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const TableView: React.FC<TableViewProps> = props => {
name,
phrases,
fileId: luFile.id,
used: luDialog ? luDialog.luIntents.includes(name) : false, // used by it's dialog or not
used: !!luDialog && luDialog.referredLuIntents.some(lu => lu.name === name), // used by it's dialog or not
state,
});
});
Expand Down
36 changes: 14 additions & 22 deletions Composer/packages/client/src/pages/notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,41 @@

/** @jsx jsx */
import { jsx } from '@emotion/core';
import { useState, useContext } from 'react';
import { useState } from 'react';
import { RouteComponentProps } from '@reach/router';

import { StoreContext } from '../../store';

import { ToolBar } from './../../components/ToolBar/index';
import useNotifications from './useNotifications';
import { NotificationList } from './NotificationList';
import { NotificationHeader } from './NotificationHeader';
import { root } from './styles';
import { INotification } from './types';
import { INotification, NotificationType } from './types';
import { navigateTo } from './../../utils';
import { convertDialogDiagnosticToUrl, toUrlUtil } from './../../utils/navigation';
import { convertPathToUrl, toUrlUtil } from './../../utils/navigation';

const Notifications: React.FC<RouteComponentProps> = () => {
const [filter, setFilter] = useState('');
const { state } = useContext(StoreContext);
const { dialogs } = state;
const notifications = useNotifications(filter);
const navigations = {
lg: (item: INotification) => {
[NotificationType.LG]: (item: INotification) => {
let url = `/language-generation/${item.id}/edit#L=${item.diagnostic.range?.start.line || 0}`;
const dividerIndex = item.id.indexOf('#');
//the format of item.id is lgFile#inlineTemplateId
if (dividerIndex > -1) {
const templateId = item.id.substring(dividerIndex + 1);
const lgFile = item.id.substring(0, dividerIndex);
const dialog = dialogs.find(d => d.lgFile === lgFile);
const lgTemplate = dialog ? dialog.lgTemplates.find(lg => lg.name === templateId) : null;
const path = lgTemplate ? lgTemplate.path : '';
if (path && dialog) {
url = toUrlUtil(dialog.id, path);
}
if (item.dialogPath) {
url = toUrlUtil(item.dialogPath);
}
navigateTo(url);
},
lu: (item: INotification) => {
navigateTo(`/dialogs/${item.id}`);
[NotificationType.LU]: (item: INotification) => {
let uri = `/language-understanding/${item.id}`;
if (item.dialogPath) {
uri = convertPathToUrl(item.id, item.dialogPath);
}
navigateTo(uri);
},
dialog: (item: INotification) => {
[NotificationType.DIALOG]: (item: INotification) => {
//path is like main.trigers[0].actions[0]
//uri = id?selected=triggers[0]&focused=triggers[0].actions[0]
const uri = convertDialogDiagnosticToUrl(item.diagnostic);
const uri = convertPathToUrl(item.id, item.dialogPath);
navigateTo(uri);
},
};
Expand Down
Loading