From 4afaa02fae51cf318ce49ef6239d9addb21e2c94 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Tue, 17 Dec 2019 16:44:40 +0800 Subject: [PATCH 01/23] update new trigger modal according to design --- .../ProjectTree/TriggerCreationModal.tsx | 88 ++++++++++++------- .../src/components/ProjectTree/styles.ts | 17 ++-- .../client/src/pages/design/index.tsx | 11 ++- .../packages/client/src/utils/dialogUtil.ts | 5 +- 4 files changed, 79 insertions(+), 42 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index ade8dd9461..c5f830c2e7 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -10,8 +10,9 @@ import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button' 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 get from 'lodash/get'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { DialogInfo } from '@bfc/indexers'; +import get from 'lodash/get'; import { addNewTrigger, @@ -25,15 +26,15 @@ import { getEventTypes, getActivityTypes, getMessageTypes, - regexRecognizerKey, } from '../../utils/dialogUtil'; import { StoreContext } from '../../store'; -import { styles, dropdownStyles, dialogWindow } from './styles'; +import { styles, dropdownStyles, dialogWindow, intent, triggerPhrases } 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'); @@ -46,21 +47,37 @@ 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'); + } 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(); @@ -71,7 +88,7 @@ export const TriggerCreationModal: React.FC = 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 dialogFile = dialogs.find(dialog => dialog.id === dialogId); const onClickSubmitButton = e => { e.preventDefault(); const errors = validateForm(formData); @@ -83,8 +100,15 @@ export const TriggerCreationModal: React.FC = props = }); return; } + const newContent = get(luFile, 'content', '') + '\n\r' + '# ' + formData.intent + '\r' + formData.triggerPhrases; + + const updateLuFile = { + id: dialogId, + content: newContent, + }; + const newDialog = addNewTrigger(dialogs, dialogId, formData); - onSubmit(newDialog); + onSubmit(newDialog, updateLuFile); onDismiss(); }; @@ -92,29 +116,23 @@ export const TriggerCreationModal: React.FC = props = 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 = (e, triggerPhrases) => { + setFormData({ ...formData, triggerPhrases: triggerPhrases }); + }; + 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; @@ -178,15 +196,23 @@ export const TriggerCreationModal: React.FC = props = data-testid={'messageTypeDropDown'} /> )} - {showIntentDropDown && ( - + )} + {showIntentFields && ( + )} diff --git a/Composer/packages/client/src/components/ProjectTree/styles.ts b/Composer/packages/client/src/components/ProjectTree/styles.ts index afb5647bd4..f6c2fc54de 100644 --- a/Composer/packages/client/src/components/ProjectTree/styles.ts +++ b/Composer/packages/client/src/components/ProjectTree/styles.ts @@ -137,7 +137,7 @@ export const dropdownStyles = { fontWeight: FontWeights.semibold, }, dropdown: { - width: '300px', + width: '400px', }, root: { paddingBottom: '20px', @@ -148,7 +148,7 @@ export const dialogWindow = css` display: flex; flex-direction: column; width: 400px; - height: 250px; + height: 300px; `; export const textFieldlabel = { @@ -162,11 +162,16 @@ export const textFieldlabel = { }; export const intent = { - fieldGroup: { - width: 200, + root: { + width: '400px', }, +}; + +export const triggerPhrases = { root: { - height: '90px', + width: '400px', + }, + fieldGroup: { + height: 80, }, - subComponentStyles: textFieldlabel, }; diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 778be7846f..25655f874f 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -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 = '') { diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index e388decfb0..e54934034c 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -21,14 +21,16 @@ interface DialogsMap { export interface TriggerFormData { errors: TriggerFormDataErrors; $type: string; - intent: string; specifiedType: string; + intent: string; + triggerPhrases: string; } export interface TriggerFormDataErrors { $type?: string; intent?: string; specifiedType?: string; + triggerPhrases?: string; } export function getDialog(dialogs: DialogInfo[], dialogId: string) { @@ -67,7 +69,6 @@ export function insert(content, path: string, position: number | undefined, data if (data.intent) { optionalAttributes.intent = data.intent; } - const newStep = { $type: data.$type, ...seedNewDialog(data.$type, {}, optionalAttributes), From 792e0588f2dbcf21a769f6ab5b616c0822584809 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Thu, 19 Dec 2019 15:58:39 +0800 Subject: [PATCH 02/23] handle comments --- .../client/src/components/ProjectTree/TriggerCreationModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index c5f830c2e7..8a70334ca4 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -100,7 +100,7 @@ export const TriggerCreationModal: React.FC = props = }); return; } - const newContent = get(luFile, 'content', '') + '\n\r' + '# ' + formData.intent + '\r' + formData.triggerPhrases; + const newContent = get(luFile, 'content', '') + '\n\n' + '# ' + formData.intent + '\n' + formData.triggerPhrases; const updateLuFile = { id: dialogId, From bca1d7c5a1ea920e8d09ce819ce1524f75125544 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Thu, 19 Dec 2019 16:01:50 +0800 Subject: [PATCH 03/23] auto-saved vscode lint setting --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0c35c861eb..916ec34905 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,4 +17,4 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true } -} +} \ No newline at end of file From b678f1ced1854001fecd81fc6cdb97eaace79664 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Thu, 19 Dec 2019 16:09:00 +0800 Subject: [PATCH 04/23] handle comments --- .vscode/settings.json | 2 +- .../client/src/components/ProjectTree/TriggerCreationModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 916ec34905..0c35c861eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,4 +17,4 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true } -} \ No newline at end of file +} diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 8a70334ca4..940a66080d 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -88,7 +88,7 @@ export const TriggerCreationModal: React.FC = 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); From c183092d0c924ea28dcc54433187194ebaa0d761 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Mon, 23 Dec 2019 12:08:45 +0800 Subject: [PATCH 05/23] css update --- Composer/packages/client/src/components/ProjectTree/styles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Composer/packages/client/src/components/ProjectTree/styles.ts b/Composer/packages/client/src/components/ProjectTree/styles.ts index f6c2fc54de..df964b5b91 100644 --- a/Composer/packages/client/src/components/ProjectTree/styles.ts +++ b/Composer/packages/client/src/components/ProjectTree/styles.ts @@ -164,6 +164,7 @@ export const textFieldlabel = { export const intent = { root: { width: '400px', + paddingBottom: '20px', }, }; From 84aafd7be0339cac638aa5a6932fb3b6d8df4659 Mon Sep 17 00:00:00 2001 From: Weitian Li Date: Thu, 16 Jan 2020 15:35:26 +0800 Subject: [PATCH 06/23] use luUtil api --- .../src/components/ProjectTree/TriggerCreationModal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 940a66080d..7392f403cb 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -27,6 +27,7 @@ import { getActivityTypes, getMessageTypes, } from '../../utils/dialogUtil'; +import { addIntent } from '../../utils/luUtil'; import { StoreContext } from '../../store'; import { styles, dropdownStyles, dialogWindow, intent, triggerPhrases } from './styles'; @@ -100,13 +101,13 @@ export const TriggerCreationModal: React.FC = props = }); return; } - const newContent = get(luFile, 'content', '') + '\n\n' + '# ' + formData.intent + '\n' + formData.triggerPhrases; + 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, updateLuFile); onDismiss(); From cca27b35c5af3ff0a969929dab57a922d998201d Mon Sep 17 00:00:00 2001 From: Long Alan Date: Tue, 4 Feb 2020 11:28:05 +0800 Subject: [PATCH 07/23] feat: [Form Editor]inline lu editor in trigger (#1872) * lu in form editor * fix naming * bugfix * fix circular reference in shared and indexer * add start and end line for each intent * add diagnostics * delete lu editor in dialog * remove notice * fix bug: composer crashed when switch page to lu page * fix tslint error * support navigation back for lu error in notification * remove unused referrence * update the lib Co-authored-by: Zhixiang Zhan Co-authored-by: liweitian Co-authored-by: leileizhang --- Composer/packages/client/src/ShellApi.ts | 12 +- .../ExtensionContainer.tsx | 12 ++ .../language-understanding/code-editor.tsx | 184 ++++-------------- .../{styles.js => styles.ts} | 0 .../language-understanding/table-view.tsx | 2 +- .../client/src/pages/notifications/index.tsx | 18 +- .../client/src/pages/notifications/types.ts | 80 +++++++- .../pages/notifications/useNotifications.tsx | 22 +-- .../packages/client/src/utils/navigation.ts | 7 +- .../obiformeditor/demo/src/index.tsx | 3 + .../src/Form/fields/RecognizerField/index.tsx | 38 +--- .../src/Form/widgets/IntentWidget.tsx | 66 +++---- .../src/Form/widgets/LuEditorWidget.tsx | 73 +++++++ .../lib/indexers/src/dialogIndexer.ts | 16 +- Composer/packages/lib/indexers/src/type.ts | 17 +- .../lib/indexers/src/utils/diagnosticUtil.ts | 18 ++ .../packages/lib/shared/src/types/shell.ts | 20 ++ 17 files changed, 317 insertions(+), 271 deletions(-) rename Composer/packages/client/src/pages/language-understanding/{styles.js => styles.ts} (100%) create mode 100644 Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index ddbacb8775..ed3dd20dea 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -217,9 +217,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.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) { @@ -227,9 +227,9 @@ export const ShellApi: React.FC = () => { 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) { @@ -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) { diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index bb37bc25b1..9ad680cfd9 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -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'); }, diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx index 577b47acdc..7c3a2b7553 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -2,167 +2,67 @@ // Licensed under the MIT License. /* eslint-disable react/display-name */ -import React, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import { LuEditor } from '@bfc/code-editor'; import get from 'lodash/get'; import debounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; -import { editor } from '@bfcomposer/monaco-editor/esm/vs/editor/editor.api'; -import { luIndexer, combineMessage, isValid, filterTemplateDiagnostics } from '@bfc/indexers'; +import { combineMessage, isValid, LuFile } from '@bfc/indexers'; import { RouteComponentProps } from '@reach/router'; -import querystring from 'query-string'; - -import { StoreContext } from '../../store'; -import * as luUtil from '../../utils/luUtil'; - -const { parse } = luIndexer; - -const lspServerPath = '/lu-language-server'; interface CodeEditorProps extends RouteComponentProps<{}> { - fileId: string; + file: LuFile; + onChange: (value: string) => {}; + errorMsg: string; } const CodeEditor: React.FC = props => { - const { actions, state } = useContext(StoreContext); - const { luFiles } = state; - const { fileId } = props; - const file = luFiles?.find(({ id }) => id === fileId); - const [diagnostics, setDiagnostics] = useState(get(file, 'diagnostics', [])); - const [httpErrorMsg, setHttpErrorMsg] = useState(''); - const [luEditor, setLuEditor] = useState(null); - - const search = props.location?.search ?? ''; - const searchSectionName = querystring.parse(search).t; - const sectionId = Array.isArray(searchSectionName) - ? searchSectionName[0] - : typeof searchSectionName === 'string' - ? searchSectionName - : undefined; - const intent = sectionId && file ? file.intents.find(({ Name }) => Name === sectionId) : undefined; - - const hash = props.location?.hash ?? ''; - const hashLine = querystring.parse(hash).L; - const line = Array.isArray(hashLine) ? +hashLine[0] : typeof hashLine === 'string' ? +hashLine : undefined; - - const inlineMode = !!intent; - const [content, setContent] = useState(intent?.Body || file?.content); + const { file, errorMsg: updateErrorMsg } = props; + const onChange = debounce(props.onChange, 500); + const diagnostics = get(file, 'diagnostics', []); + const [content, setContent] = useState(get(file, 'content', '')); + const fileId = file && file.id; useEffect(() => { - // reset content with file.content initial state - if (!file || isEmpty(file) || content) return; - const value = intent ? intent.Body : file.content; + // reset content with file.content's initial state + if (isEmpty(file)) return; + setContent(file.content); + }, [fileId]); + + // local content maybe invalid and should always sync real-time + // file.content assume to be load from server + const _onChange = value => { setContent(value); - }, [file, sectionId]); - - const errorMsg = useMemo(() => { - const currentDiagnostics = inlineMode && intent ? filterTemplateDiagnostics(diagnostics, intent) : diagnostics; - const isInvalid = !isValid(currentDiagnostics); - return isInvalid ? combineMessage(diagnostics) : httpErrorMsg; - }, [diagnostics, httpErrorMsg]); - - const editorDidMount = (luEditor: editor.IStandaloneCodeEditor) => { - setLuEditor(luEditor); + // TODO: validate before request update server like lg, when luParser is ready + onChange(value); }; - useEffect(() => { - if (luEditor && line !== undefined) { - window.requestAnimationFrame(() => { - luEditor.revealLine(line); - luEditor.focus(); - luEditor.setPosition({ lineNumber: line, column: 1 }); - }); - } - }, [line, luEditor]); - - const updateLuIntent = useMemo( - () => - debounce((Body: string) => { - if (!file || !intent) return; - const { Name } = intent; - const payload = { - file, - intentName: Name, - intent: { - Name, - Body, - }, - }; - actions.updateLuIntent(payload); - }, 500), - [file, intent] - ); - - const updateLuFile = useMemo( - () => - debounce((content: string) => { - if (!file) return; - const { id } = file; - const payload = { - id, - content, - }; - actions.updateLuFile(payload); - }, 500), - [file] - ); - - const updateDiagnostics = useMemo( - () => - debounce((value: string) => { - if (!file) return; - const { id } = file; - if (inlineMode) { - if (!intent) return; - const { Name } = intent; - const { content } = file; - try { - const newContent = luUtil.updateIntent(content, Name, { - Name, - Body: value, - }); - const { diagnostics } = parse(newContent, id); - setDiagnostics(diagnostics); - } catch (error) { - setHttpErrorMsg(error.error); - } - } else { - const { diagnostics } = parse(value, id); - setDiagnostics(diagnostics); - } - }, 1000), - [file, intent] - ); - - const _onChange = useCallback( - value => { - setContent(value); - updateDiagnostics(value); - if (!file) return; - if (inlineMode) { - updateLuIntent(value); - } else { - updateLuFile(value); - } - }, - [file, intent] - ); - - const luOption = { - fileId, - sectionId: intent?.Name, - }; + // diagnostics is load file error, + // updateErrorMsg is save file return error. + const isInvalid = !isValid(file.diagnostics) || updateErrorMsg !== ''; + const errorMsg = isInvalid ? `${combineMessage(diagnostics)}\n ${updateErrorMsg}` : ''; return ( ); diff --git a/Composer/packages/client/src/pages/language-understanding/styles.js b/Composer/packages/client/src/pages/language-understanding/styles.ts similarity index 100% rename from Composer/packages/client/src/pages/language-understanding/styles.js rename to Composer/packages/client/src/pages/language-understanding/styles.ts diff --git a/Composer/packages/client/src/pages/language-understanding/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index b04afef158..8840ecfba7 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -79,7 +79,7 @@ const TableView: React.FC = props => { name, phrases, fileId: luFile.id, - used: luDialog ? luDialog.luIntents.includes(name) : false, // used by it's dialog or not + used: luDialog ? !!luDialog.referredLuIntents.find(lu => lu.name === name) : false, // used by it's dialog or not state, }); }); diff --git a/Composer/packages/client/src/pages/notifications/index.tsx b/Composer/packages/client/src/pages/notifications/index.tsx index cccac44dc3..f32a274ab8 100644 --- a/Composer/packages/client/src/pages/notifications/index.tsx +++ b/Composer/packages/client/src/pages/notifications/index.tsx @@ -13,9 +13,9 @@ 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 = () => { const [filter, setFilter] = useState(''); @@ -23,7 +23,7 @@ const Notifications: React.FC = () => { 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 @@ -39,13 +39,17 @@ const Notifications: React.FC = () => { } 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); }, }; diff --git a/Composer/packages/client/src/pages/notifications/types.ts b/Composer/packages/client/src/pages/notifications/types.ts index bce5eef0a0..b59583ca01 100644 --- a/Composer/packages/client/src/pages/notifications/types.ts +++ b/Composer/packages/client/src/pages/notifications/types.ts @@ -1,13 +1,89 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { Diagnostic, createSingleMessage, DialogInfo, LuFile } from '@bfc/indexers'; + +import { replaceDialogDiagnosticLabel } from '../../utils'; + +export const DiagnosticSeverity = ['Error', 'Warning']; //'Information', 'Hint' + +export enum NotificationType { + DIALOG, + LG, + LU, +} export interface INotification { id: string; severity: string; - type: string; + type: NotificationType; location: string; message: string; diagnostic: any; + dialogPath?: string; //the data path in dialog } -export const DiagnosticSeverity = ['Error', 'Warning']; //'Information', 'Hint' +export class DialogNotification implements INotification { + id: string; + severity: string; + type: NotificationType; + location: string; + message: string; + diagnostic: Diagnostic; + dialogPath?: string; + constructor(id: string, location: string, diagnostic: Diagnostic) { + this.id = id; + this.severity = DiagnosticSeverity[diagnostic.severity] || ''; + this.message = `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`; + this.diagnostic = diagnostic; + this.location = location; + this.type = NotificationType.DIALOG; + this.dialogPath = diagnostic.path; + } +} + +export class LgNotification implements INotification { + id: string; + severity: string; + type: NotificationType; + location: string; + message: string; + diagnostic: Diagnostic; + dialogPath?: string; + constructor(id: string, location: string, diagnostic: Diagnostic) { + this.id = id; + this.severity = DiagnosticSeverity[diagnostic.severity] || ''; + this.message = createSingleMessage(diagnostic); + this.diagnostic = diagnostic; + this.location = location; + this.type = NotificationType.LG; + } +} + +export class LuNotification implements INotification { + id: string; + severity: string; + type: NotificationType; + location: string; + message: string; + diagnostic: Diagnostic; + dialogPath?: string; + constructor(id: string, location: string, diagnostic: Diagnostic, luFile: LuFile, dialogs: DialogInfo[]) { + this.id = id; + this.severity = DiagnosticSeverity[diagnostic.severity] || ''; + this.message = createSingleMessage(diagnostic); + this.diagnostic = diagnostic; + this.location = location; + this.type = NotificationType.LU; + this.dialogPath = this.findDialogPath(luFile, dialogs, diagnostic); + } + + private findDialogPath(luFile: LuFile, dialogs: DialogInfo[], d: Diagnostic) { + const intentName = luFile.intents.find(intent => { + const { range } = intent; + if (!range) return false; + return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + })?.Name; + + return dialogs.find(dialog => dialog.id === luFile.id)?.referredLuIntents.find(lu => lu.name === intentName)?.path; + } +} diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 8865060dc7..ec02a97bb5 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -2,14 +2,12 @@ // Licensed under the MIT License. import { useContext, useMemo } from 'react'; -import { createSingleMessage } from '@bfc/indexers'; import get from 'lodash/get'; import { LgNamePattern } from '@bfc/shared'; import { StoreContext } from '../../store'; -import { replaceDialogDiagnosticLabel } from '../../utils'; -import { INotification, DiagnosticSeverity } from './types'; +import { INotification, DialogNotification, LuNotification, LgNotification } from './types'; import { getReferredFiles } from './../../utils/luUtil'; export default function useNotifications(filter?: string) { const { state } = useContext(StoreContext); @@ -19,27 +17,13 @@ export default function useNotifications(filter?: string) { dialogs.forEach(dialog => { dialog.diagnostics.map(diagnostic => { const location = `${dialog.id}.dialog`; - notifactions.push({ - type: 'dialog', - location, - message: `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`, - severity: DiagnosticSeverity[diagnostic.severity] || '', - diagnostic, - id: dialog.id, - }); + notifactions.push(new DialogNotification(dialog.id, location, diagnostic)); }); }); getReferredFiles(luFiles, dialogs).forEach(lufile => { lufile.diagnostics.map(diagnostic => { const location = `${lufile.id}.lu`; - notifactions.push({ - type: 'lu', - location, - message: createSingleMessage(diagnostic), - severity: 'Error', - diagnostic, - id: lufile.id, - }); + notifactions.push(new LuNotification(lufile.id, location, diagnostic, lufile, dialogs)); }); }); lgFiles.forEach(lgFile => { diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index 415f6285a1..2d2c81f377 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -3,7 +3,6 @@ import cloneDeep from 'lodash/cloneDeep'; import { navigate, NavigateOptions } from '@reach/router'; -import { Diagnostic } from '@bfc/indexers'; import { BreadcrumbItem, DesignPageLocation } from '../store/types'; @@ -77,13 +76,11 @@ interface NavigationState { breadcrumb: BreadcrumbItem[]; } -export function convertDialogDiagnosticToUrl(diagnostic: Diagnostic): string { +export function convertPathToUrl(id: string, path?: string): string { //path is like main.trigers[0].actions[0] //uri = id?selected=triggers[0]&focused=triggers[0].actions[0] - const { path, source } = diagnostic; - if (!source) return ''; - let uri = `/dialogs/${source}`; + let uri = `/dialogs/${id}`; if (!path) return uri; const items = path.split('#'); diff --git a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx index 8897c9d207..a399e293d7 100644 --- a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx +++ b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx @@ -173,6 +173,9 @@ const mockShellApi = [ 'getLgTemplates', 'createLgTemplate', 'updateLgTemplate', + 'addLuIntent', + 'updateLuIntent', + 'removeLuIntent', 'validateExpression', 'onFocusSteps', 'onFocusEvent', diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx index a4ba6f1048..5a490cfda5 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx @@ -1,28 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState, ReactElement, Suspense, useEffect } from 'react'; +import React, { useState, ReactElement } from 'react'; import formatMessage from 'format-message'; import { FieldProps } from '@bfcomposer/react-jsonschema-form'; import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import { MicrosoftIRecognizer } from '@bfc/shared'; -import { LuFile, combineMessage } from '@bfc/indexers'; +import { LuFile } from '@bfc/indexers'; import { BaseField } from '../BaseField'; -import { LoadingSpinner } from '../../../LoadingSpinner'; import ToggleEditor from './ToggleEditor'; import RegexEditor from './RegexEditor'; import './styles.css'; -const InlineLuEditor = React.lazy(() => import('./InlineLuEditor')); - export const RecognizerField: React.FC> = props => { const { formData } = props; const [loading, setLoading] = useState(false); - const [errorMsg, setErrorMsg] = useState(''); const { formContext: { luFiles, shellApi, currentDialog }, @@ -32,19 +28,6 @@ export const RecognizerField: React.FC> = props const isRegex = typeof formData === 'object' && formData.$type === 'Microsoft.RegexRecognizer'; const currentDialogId = currentDialog.id; const selectedFile: LuFile | void = luFiles.find(f => f.id === currentDialogId); - const isLuFileSelected = Boolean( - selectedFile && typeof props.formData === 'string' && props.formData.startsWith(selectedFile.id) - ); - - //make the inline editor show error message - useEffect(() => { - if (selectedFile && selectedFile.diagnostics.length > 0) { - const msg = combineMessage(selectedFile.diagnostics); - setErrorMsg(msg); - } else { - setErrorMsg(''); - } - }, [selectedFile]); const handleChange = (_, option?: IDropdownOption): void => { if (option) { @@ -144,23 +127,8 @@ export const RecognizerField: React.FC> = props responsiveMode={ResponsiveMode.large} onRenderTitle={onRenderTitle} /> - + {() => { - if (selectedFile && isLuFileSelected) { - const updateLuFile = (newValue?: string): void => { - shellApi.updateLuFile({ id: selectedFile.id, content: newValue }).catch(setErrorMsg); - }; - - return ( - }> - - - ); - } if (isRegex) { return ; } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx index dbe9bef687..8d083dd329 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx @@ -5,11 +5,12 @@ import React from 'react'; import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import formatMessage from 'format-message'; import { RegexRecognizer } from '@bfc/shared'; -import { LuFile, DialogInfo } from '@bfc/indexers'; +import { DialogInfo } from '@bfc/indexers'; -import { BFDWidgetProps, FormContext } from '../types'; +import { BFDWidgetProps } from '../types'; import { WidgetLabel } from './WidgetLabel'; +import LuEditorWidget from './LuEditorWidget'; const EMPTY_OPTION = { key: '', text: '' }; @@ -48,42 +49,26 @@ function regexIntentOptions(currentDialog: DialogInfo): IDropdownOption[] { return options; } -function luIntentOptions(formContext: FormContext): IDropdownOption[] { - const luFile: LuFile | void = formContext.luFiles.find(f => f.id === formContext.currentDialog.id); - let options: IDropdownOption[] = [EMPTY_OPTION]; - - if (luFile) { - const intents: { name: string }[] = luFile.intents.map(({ Name: name }) => { - return { - name, - }; - }); - - options = options.concat( - intents.map(i => ({ - key: i.name, - text: i.name, - })) - ); - } - - return options; -} - export const IntentWidget: React.FC = props => { const { disabled, onChange, id, onFocus, onBlur, value, formContext, placeholder, label, schema } = props; const { description } = schema; + const { currentDialog } = formContext; let options: IDropdownOption[] = []; + let widgetLabel = label; + let isLuisSelected = false; - switch (recognizerType(formContext.currentDialog)) { + switch (recognizerType(currentDialog)) { case RecognizerType.regex: - options = regexIntentOptions(formContext.currentDialog); + options = regexIntentOptions(currentDialog); + isLuisSelected = false; break; case RecognizerType.luis: - options = luIntentOptions(formContext); + widgetLabel = `Trigger phrases(intent name: #${value || ''})`; + isLuisSelected = true; break; default: options = [EMPTY_OPTION]; + isLuisSelected = false; break; } @@ -95,18 +80,21 @@ export const IntentWidget: React.FC = props => { return ( <> - - onBlur && onBlur(id, value)} - onChange={handleChange} - onFocus={() => onFocus && onFocus(id, value)} - options={options} - selectedKey={value || null} - responsiveMode={ResponsiveMode.large} - disabled={disabled || options.length === 1} - placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} - /> + + {!isLuisSelected && ( + onBlur && onBlur(id, value)} + onChange={handleChange} + onFocus={() => onFocus && onFocus(id, value)} + options={options} + selectedKey={value || null} + responsiveMode={ResponsiveMode.large} + disabled={disabled || options.length === 1} + placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} + /> + )} + {isLuisSelected && } ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx new file mode 100644 index 0000000000..0f019352ac --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState, useMemo, useEffect } from 'react'; +import { LuEditor } from '@bfc/code-editor'; +import debounce from 'lodash/debounce'; +import { LuIntentSection } from '@bfc/shared'; +import { LuFile, filterSectionDiagnostics } from '@bfc/indexers'; + +import { FormContext } from '../types'; + +interface LuEditorWidgetProps { + formContext: FormContext; + name: string; + height?: number | string; + onChange: (template?: string) => void; +} + +export const LuEditorWidget: React.FC = props => { + const { formContext, name, height = 250 } = props; + const luFileId = formContext.currentDialog.id; + const luFile: LuFile | null = formContext.luFiles.find(f => f.id === luFileId); + const luIntent: LuIntentSection = (luFile && luFile.intents.find(intent => intent.Name === name)) || { + Name: name, + Body: '', + }; + + const updateLuIntent = useMemo( + () => + debounce((body: string) => { + formContext.shellApi.updateLuIntent(luFileId, name, { Name: name, Body: body }).catch(() => {}); + }, 500), + [name, luFileId] + ); + + const diagnostic = luFile && filterSectionDiagnostics(luFile.diagnostics, luIntent)[0]; + + const errorMsg = diagnostic + ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] + : ''; + + const [localValue, setLocalValue] = useState(luIntent.Body); + + // updating localValue when getting newest luIntent Data + // it will be deleted after leilei's pr: fix: Undo / redo behavior on LG resources + useEffect(() => { + if (!localValue) { + setLocalValue(luIntent.Body); + } + }, [luIntent.Body]); + const onChange = (body: string) => { + setLocalValue(body); + if (luFileId) { + if (body) { + updateLuIntent(body); + } else { + updateLuIntent.flush(); + formContext.shellApi.removeLuIntent(luFileId, name); + } + } + }; + + // update the template on mount to get validation + useEffect(() => { + if (localValue) { + updateLuIntent(localValue); + } + }, []); + + return ; +}; + +export default LuEditorWidget; diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index 0940379b96..eb36ee6ae6 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -5,8 +5,9 @@ import has from 'lodash/has'; import uniq from 'lodash/uniq'; import { extractLgTemplateRefs } from '@bfc/shared'; +import { createPath } from './dialogUtils/dialogChecker'; import { checkerFuncs } from './dialogUtils/dialogChecker'; -import { ITrigger, DialogInfo, FileInfo, LgTemplateJsonPath } from './type'; +import { ITrigger, DialogInfo, FileInfo, LgTemplateJsonPath, ReferredLuIntents } from './type'; import { JsonWalk, VisitorFunc } from './utils/jsonWalk'; import { getBaseName } from './utils/help'; import { Diagnostic } from './diagnostic'; @@ -70,8 +71,8 @@ function ExtractLgTemplates(id, dialog): LgTemplateJsonPath[] { } // find out all lu intents given dialog -function ExtractLuIntents(dialog): string[] { - const intents: string[] = []; +function ExtractLuIntents(dialog, id: string): ReferredLuIntents[] { + const intents: ReferredLuIntents[] = []; /** * * @param path , jsonPath string * @param value , current node value * @@ -81,11 +82,14 @@ function ExtractLuIntents(dialog): string[] { // it's a valid schema dialog node. if (has(value, '$type') && value.$type === 'Microsoft.OnIntent') { const intentName = value.intent; - intents.push(intentName); + intents.push({ + name: intentName, + path: createPath(path, value.$type), + }); } return false; }; - JsonWalk('$', dialog, visitor); + JsonWalk(id, dialog, visitor); return uniq(intents); } @@ -195,8 +199,8 @@ function parse(id: string, content: any, schema: any) { diagnostics: validate(id, content, schema), referredDialogs: ExtractReferredDialogs(content), lgTemplates: ExtractLgTemplates(id, content), - luIntents: ExtractLuIntents(content), userDefinedVariables: ExtractMemoryPaths(content), + referredLuIntents: ExtractLuIntents(content, id), luFile: getBaseName(luFile, '.lu'), lgFile: getBaseName(lgFile, '.lg'), triggers: ExtractTriggers(content), diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index 5e9f3d62ce..5b47641a57 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { LuIntentSection } from '@bfc/shared'; + import { Diagnostic } from './diagnostic'; export interface FileInfo { @@ -17,6 +19,11 @@ export interface ITrigger { isIntent: boolean; } +export interface ReferredLuIntents { + name: string; + path: string; +} + export interface DialogInfo { content: any; diagnostics: Diagnostic[]; @@ -26,7 +33,7 @@ export interface DialogInfo { lgFile: string; lgTemplates: LgTemplateJsonPath[]; luFile: string; - luIntents: string[]; + referredLuIntents: ReferredLuIntents[]; referredDialogs: string[]; relativePath: string; userDefinedVariables: string[]; @@ -62,14 +69,6 @@ export interface LuEntity { Name: string; } -export interface LuIntentSection { - Name: string; - Body: string; - Entities?: LuEntity[]; - Children?: LuIntentSection[]; - range?: CodeRange; -} - export interface LuFile { id: string; relativePath: string; diff --git a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts index 41695327c2..079ed43c37 100644 --- a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { LuIntentSection } from '@bfc/shared'; + import { Diagnostic, DiagnosticSeverity, Range, Position } from '../diagnostic'; import { CodeRange } from '../type'; @@ -46,6 +48,22 @@ export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: }); } +export function filterSectionDiagnostics(diagnostics: Diagnostic[], section: LuIntentSection): Diagnostic[] { + const { range } = section; + if (!range) return diagnostics; + const filteredDiags = diagnostics.filter(d => { + return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + }); + const offset = range.startLineNumber; + return filteredDiags.map(d => { + const { range } = d; + if (range) { + d.range = offsetRange(range, offset); + } + return d; + }); +} + export function findErrors(diagnostics: Diagnostic[]): Diagnostic[] { return diagnostics.filter(d => d.severity === DiagnosticSeverity.Error); } diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 0077441749..521d3600a1 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -3,6 +3,23 @@ import { LGTemplate as LgTemplate } from 'botbuilder-lg'; +export interface LuIntentSection { + Name: string; + Body: string; + Entities?: LuEntity[]; + Children?: LuIntentSection[]; + range?: CodeRange; +} + +export interface CodeRange { + startLineNumber: number; + endLineNumber: number; +} + +export interface LuEntity { + Name: string; +} + export interface EditorSchema { content?: { fieldTemplateOverrides?: any; @@ -53,6 +70,9 @@ export interface ShellApi { updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; removeLgTemplate: (id: string, templateName: string) => Promise; removeLgTemplates: (id: string, templateNames: string[]) => Promise; + addLuIntent: (id: string, intent: LuIntentSection | null) => Promise; + updateLuIntent: (id: string, intentName: string, intent: LuIntentSection | null) => Promise; + removeLuIntent: (id: string, intentName: string) => Promise; createDialog: () => Promise; validateExpression: (expression?: string) => Promise; // TODO: fix these types From 07c5220ca3bc8e5f1626782a7d2088d1a76c46fb Mon Sep 17 00:00:00 2001 From: liweitian Date: Mon, 10 Feb 2020 18:07:17 +0800 Subject: [PATCH 08/23] switch tsx back to js --- .../{code-editor.tsx => code-editor.js} | 23 ++++++++----------- .../{styles.ts => styles.js} | 0 2 files changed, 9 insertions(+), 14 deletions(-) rename Composer/packages/client/src/pages/language-understanding/{code-editor.tsx => code-editor.js} (77%) rename Composer/packages/client/src/pages/language-understanding/{styles.ts => styles.js} (100%) diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.js similarity index 77% rename from Composer/packages/client/src/pages/language-understanding/code-editor.tsx rename to Composer/packages/client/src/pages/language-understanding/code-editor.js index 7c3a2b7553..a51ba5b456 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.js @@ -3,20 +3,14 @@ /* eslint-disable react/display-name */ import React, { useState, useEffect } from 'react'; +import { PropTypes } from 'prop-types'; import { LuEditor } from '@bfc/code-editor'; import get from 'lodash/get'; import debounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; -import { combineMessage, isValid, LuFile } from '@bfc/indexers'; -import { RouteComponentProps } from '@reach/router'; +import { combineMessage, isValid } from '@bfc/indexers'; -interface CodeEditorProps extends RouteComponentProps<{}> { - file: LuFile; - onChange: (value: string) => {}; - errorMsg: string; -} - -const CodeEditor: React.FC = props => { +export default function CodeEditor(props) { const { file, errorMsg: updateErrorMsg } = props; const onChange = debounce(props.onChange, 500); const diagnostics = get(file, 'diagnostics', []); @@ -44,9 +38,6 @@ const CodeEditor: React.FC = props => { return ( = props => { onChange={_onChange} /> ); -}; +} -export default CodeEditor; +CodeEditor.propTypes = { + file: PropTypes.object, + onChange: PropTypes.func, + errorMsg: PropTypes.string, +}; diff --git a/Composer/packages/client/src/pages/language-understanding/styles.ts b/Composer/packages/client/src/pages/language-understanding/styles.js similarity index 100% rename from Composer/packages/client/src/pages/language-understanding/styles.ts rename to Composer/packages/client/src/pages/language-understanding/styles.js From 29c6df69eb137ec32923179f1a1ce5347c930297 Mon Sep 17 00:00:00 2001 From: liweitian Date: Mon, 10 Feb 2020 20:01:17 +0800 Subject: [PATCH 09/23] switch js to tsx --- .../{code-editor.js => code-editor.tsx} | 23 +++++++++++-------- .../{styles.js => styles.ts} | 0 2 files changed, 14 insertions(+), 9 deletions(-) rename Composer/packages/client/src/pages/language-understanding/{code-editor.js => code-editor.tsx} (77%) rename Composer/packages/client/src/pages/language-understanding/{styles.js => styles.ts} (100%) diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.js b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx similarity index 77% rename from Composer/packages/client/src/pages/language-understanding/code-editor.js rename to Composer/packages/client/src/pages/language-understanding/code-editor.tsx index a51ba5b456..7c3a2b7553 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.js +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -3,14 +3,20 @@ /* eslint-disable react/display-name */ import React, { useState, useEffect } from 'react'; -import { PropTypes } from 'prop-types'; import { LuEditor } from '@bfc/code-editor'; import get from 'lodash/get'; import debounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; -import { combineMessage, isValid } from '@bfc/indexers'; +import { combineMessage, isValid, LuFile } from '@bfc/indexers'; +import { RouteComponentProps } from '@reach/router'; -export default function CodeEditor(props) { +interface CodeEditorProps extends RouteComponentProps<{}> { + file: LuFile; + onChange: (value: string) => {}; + errorMsg: string; +} + +const CodeEditor: React.FC = props => { const { file, errorMsg: updateErrorMsg } = props; const onChange = debounce(props.onChange, 500); const diagnostics = get(file, 'diagnostics', []); @@ -38,6 +44,9 @@ export default function CodeEditor(props) { return ( ); -} - -CodeEditor.propTypes = { - file: PropTypes.object, - onChange: PropTypes.func, - errorMsg: PropTypes.string, }; + +export default CodeEditor; diff --git a/Composer/packages/client/src/pages/language-understanding/styles.js b/Composer/packages/client/src/pages/language-understanding/styles.ts similarity index 100% rename from Composer/packages/client/src/pages/language-understanding/styles.js rename to Composer/packages/client/src/pages/language-understanding/styles.ts From 090872ee50a847c0ba8368992b0c46352690f9b5 Mon Sep 17 00:00:00 2001 From: liweitian Date: Mon, 10 Feb 2020 22:36:51 +0800 Subject: [PATCH 10/23] add editor in trigger wizard --- .../ProjectTree/TriggerCreationModal.tsx | 29 ++++++++++++------- .../src/components/ProjectTree/styles.ts | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 7392f403cb..6e6675c6dc 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -7,12 +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 } from '@bfc/indexers'; +import { DialogInfo, luIndexer, combineMessage } from '@bfc/indexers'; import get from 'lodash/get'; +import { LuEditor } from '@bfc/code-editor'; import { addNewTrigger, @@ -30,7 +32,7 @@ import { import { addIntent } from '../../utils/luUtil'; import { StoreContext } from '../../store'; -import { styles, dropdownStyles, dialogWindow, intent, triggerPhrases } from './styles'; +import { styles, dropdownStyles, dialogWindow, intent } from './styles'; const nameRegex = /^[a-zA-Z0-9-_.]+$/; const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { @@ -58,6 +60,9 @@ const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { if (!triggerPhrases) { errors.triggerPhrases = formatMessage('Please input trigger phrases'); } + if (data.errors.triggerPhrases) { + errors.triggerPhrases = data.errors.triggerPhrases; + } return errors; }; @@ -125,8 +130,12 @@ export const TriggerCreationModal: React.FC = props = setFormData({ ...formData, intent: name }); }; - const onTriggerPhrasesChange = (e, triggerPhrases) => { - setFormData({ ...formData, triggerPhrases: triggerPhrases }); + 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(); @@ -206,14 +215,14 @@ export const TriggerCreationModal: React.FC = props = data-testid="TriggerName" /> )} + {showIntentFields && } {showIntentFields && ( - )} diff --git a/Composer/packages/client/src/components/ProjectTree/styles.ts b/Composer/packages/client/src/components/ProjectTree/styles.ts index df964b5b91..0d53763b9a 100644 --- a/Composer/packages/client/src/components/ProjectTree/styles.ts +++ b/Composer/packages/client/src/components/ProjectTree/styles.ts @@ -148,7 +148,7 @@ export const dialogWindow = css` display: flex; flex-direction: column; width: 400px; - height: 300px; + min-height: 300px; `; export const textFieldlabel = { From d73126de31599c07e4d5b42bdb97fc22cb54acbd Mon Sep 17 00:00:00 2001 From: liweitian Date: Tue, 11 Feb 2020 21:31:19 +0800 Subject: [PATCH 11/23] fix unit test --- .../__tests__/components/design.test.js | 1 + .../__mocks__/TriggerCreationModal.tsx | 217 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx diff --git a/Composer/packages/client/__tests__/components/design.test.js b/Composer/packages/client/__tests__/components/design.test.js index ead441d63f..a46d188f2d 100644 --- a/Composer/packages/client/__tests__/components/design.test.js +++ b/Composer/packages/client/__tests__/components/design.test.js @@ -10,6 +10,7 @@ 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('./../../src/components/ProjectTree/TriggerCreationModal'); describe('', () => { it('should render the ProjectTree', async () => { const dialogId = 'Main'; diff --git a/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx new file mode 100644 index 0000000000..16cf31b36f --- /dev/null +++ b/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +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 } from '@bfc/indexers'; +import get from 'lodash/get'; + +import { + addNewTrigger, + getTriggerTypes, + TriggerFormData, + TriggerFormDataErrors, + eventTypeKey, + intentTypeKey, + activityTypeKey, + messageTypeKey, + getEventTypes, + getActivityTypes, + getMessageTypes, +} from '../../../utils/dialogUtil'; +import { addIntent } from '../../../utils/luUtil'; +import { StoreContext } from '../../../store'; +import { styles, dropdownStyles, dialogWindow, intent } from '../styles'; + +const nameRegex = /^[a-zA-Z0-9-_.]+$/; +const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { + const errors: TriggerFormDataErrors = {}; + const { $type, specifiedType, intent, triggerPhrases } = data; + + if ($type === eventTypeKey && !specifiedType) { + errors.specifiedType = formatMessage('Please select a event type'); + } + + if ($type === activityTypeKey && !specifiedType) { + errors.specifiedType = formatMessage('Please select an activity type'); + } + + 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, luFilePayload: LuFilePayload) => void; +} + +const initialFormData: TriggerFormData = { + errors: {}, + $type: intentTypeKey, + specifiedType: '', + intent: '', + triggerPhrases: '', +}; + +const triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); + +export const TriggerCreationModal: React.FC = props => { + const { isOpen, onDismiss, onSubmit, dialogId } = props; + const [formData, setFormData] = useState(initialFormData); + const { state } = useContext(StoreContext); + const { dialogs, luFiles } = state; + const luFile = luFiles.find(lu => lu.id === dialogId); + + const onClickSubmitButton = e => { + e.preventDefault(); + const errors = validateForm(formData); + + if (Object.keys(errors).length) { + setFormData({ + ...formData, + errors, + }); + 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, updateLuFile); + onDismiss(); + }; + + const onSelectTriggerType = (e, option) => { + setFormData({ ...initialFormData, $type: option.key }); + }; + + const onSelectSpecifiedTypeType = (e, option) => { + setFormData({ ...formData, specifiedType: option.key }); + }; + + const onNameChange = (e, name) => { + setFormData({ ...formData, intent: name }); + }; + + const eventTypes: IDropdownOption[] = getEventTypes(); + const activityTypes: IDropdownOption[] = getActivityTypes(); + const messageTypes: IDropdownOption[] = getMessageTypes(); + + const showIntentFields = formData.$type === intentTypeKey; + const showEventDropDown = formData.$type === eventTypeKey; + const showActivityDropDown = formData.$type === activityTypeKey; + const showMessageDropDown = formData.$type === messageTypeKey; + + return ( + + ); +}; From b6eca7df8bd37eca7075920961c73ab377c67e34 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 12 Feb 2020 17:46:04 +0800 Subject: [PATCH 12/23] when lu is not valid, don't do update --- .../lib/indexers/__tests__/luUtil.test.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts index 916061b82d..5eaa009cff 100644 --- a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts @@ -90,14 +90,13 @@ describe('LU Section CRUD test', () => { Name: 'CheckEmail', Body: `- check my email - show my emails -`, +@`, }; const invalidFileContent = `#CheckEmail -- check my email -- show my emails -@`; - + - check my email + - show my emails + @`; const invalidIntent = { Name: 'CheckEmail', Body: `- check my email @@ -105,20 +104,10 @@ describe('LU Section CRUD test', () => { @`, }; - const invalidIntent4 = { - Name: 'CheckEmail', - Body: `- check my email -- show my emails - -# UnexpectedIntentDefination -- unexpected intent body -`, - }; // intent invalid const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); expect(updatedContent2).toEqual(validFileContent); - const updatedContent4 = updateIntent(validFileContent, intentName, invalidIntent4); - expect(updatedContent4).toEqual(validFileContent); + // file invalid const updatedContent3 = updateIntent(invalidFileContent, intentName, validIntent); expect(updatedContent3).toEqual(invalidFileContent); From d0ff79a2295216ff15e9f368dfff06da89afe6f4 Mon Sep 17 00:00:00 2001 From: liweitian Date: Tue, 25 Feb 2020 20:03:53 +0800 Subject: [PATCH 13/23] handle comments --- .../client/src/pages/notifications/index.tsx | 16 +--- .../client/src/pages/notifications/types.ts | 73 ++++++++++--------- .../pages/notifications/useNotifications.tsx | 15 +--- .../packages/client/src/utils/navigation.ts | 4 +- .../lib/indexers/src/utils/diagnosticUtil.ts | 19 ++++- 5 files changed, 65 insertions(+), 62 deletions(-) diff --git a/Composer/packages/client/src/pages/notifications/index.tsx b/Composer/packages/client/src/pages/notifications/index.tsx index f32a274ab8..ea4644eda6 100644 --- a/Composer/packages/client/src/pages/notifications/index.tsx +++ b/Composer/packages/client/src/pages/notifications/index.tsx @@ -19,23 +19,13 @@ import { convertPathToUrl, toUrlUtil } from './../../utils/navigation'; const Notifications: React.FC = () => { const [filter, setFilter] = useState(''); - const { state } = useContext(StoreContext); - const { dialogs } = state; const notifications = useNotifications(filter); const navigations = { [NotificationType.LG]: (item: INotification) => { - let url = `/language-generation/${item.id}/edit#L=${item.diagnostic.range?.start.line || 0}`; - const dividerIndex = item.id.indexOf('#'); + let url = `/language-generation/common/edit#L=${item.diagnostic.range?.start.line || 0}`; //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); }, diff --git a/Composer/packages/client/src/pages/notifications/types.ts b/Composer/packages/client/src/pages/notifications/types.ts index b59583ca01..957cd17aa5 100644 --- a/Composer/packages/client/src/pages/notifications/types.ts +++ b/Composer/packages/client/src/pages/notifications/types.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Diagnostic, createSingleMessage, DialogInfo, LuFile } from '@bfc/indexers'; +import { Diagnostic, createSingleMessage, DialogInfo, LuFile, isDiagnosticWithInRange } from '@bfc/indexers'; import { replaceDialogDiagnosticLabel } from '../../utils'; - export const DiagnosticSeverity = ['Error', 'Warning']; //'Information', 'Hint' export enum NotificationType { DIALOG, LG, LU, + GENERAL, } export interface INotification { @@ -22,66 +22,69 @@ export interface INotification { dialogPath?: string; //the data path in dialog } -export class DialogNotification implements INotification { +export abstract class Notification implements INotification { id: string; severity: string; - type: NotificationType; + type = NotificationType.GENERAL; location: string; - message: string; + message = ''; diagnostic: Diagnostic; dialogPath?: string; constructor(id: string, location: string, diagnostic: Diagnostic) { this.id = id; this.severity = DiagnosticSeverity[diagnostic.severity] || ''; - this.message = `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`; this.diagnostic = diagnostic; this.location = location; - this.type = NotificationType.DIALOG; - this.dialogPath = diagnostic.path; } } -export class LgNotification implements INotification { - id: string; - severity: string; - type: NotificationType; - location: string; - message: string; - diagnostic: Diagnostic; +export class DialogNotification extends Notification { + type = NotificationType.DIALOG; dialogPath?: string; constructor(id: string, location: string, diagnostic: Diagnostic) { - this.id = id; - this.severity = DiagnosticSeverity[diagnostic.severity] || ''; - this.message = createSingleMessage(diagnostic); - this.diagnostic = diagnostic; - this.location = location; - this.type = NotificationType.LG; + super(id, location, diagnostic); + this.message = `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`; + this.dialogPath = diagnostic.path; } } -export class LuNotification implements INotification { - id: string; - severity: string; - type: NotificationType; - location: string; - message: string; - diagnostic: Diagnostic; +export class LgNotification extends Notification { + type = NotificationType.LG; dialogPath?: string; - constructor(id: string, location: string, diagnostic: Diagnostic, luFile: LuFile, dialogs: DialogInfo[]) { - this.id = id; - this.severity = DiagnosticSeverity[diagnostic.severity] || ''; + constructor(id: string, location: string, diagnostic: Diagnostic, dialogs: DialogInfo[]) { + super(id, location, diagnostic); this.message = createSingleMessage(diagnostic); - this.diagnostic = diagnostic; - this.location = location; - this.type = NotificationType.LU; + this.dialogPath = this.findDialogPath(dialogs, id); + } + private findDialogPath(dialogs: DialogInfo[], id: string) { + const dividerIndex = id.indexOf('#'); + if (dividerIndex > -1) { + const templateId = id.substring(dividerIndex + 1); + const lgFile = 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 : ''; + return path; + } + } +} + +export class LuNotification extends Notification { + type = NotificationType.LU; + constructor(id: string, location: string, diagnostic: Diagnostic, luFile: LuFile, dialogs: DialogInfo[]) { + super(id, location, diagnostic); this.dialogPath = this.findDialogPath(luFile, dialogs, diagnostic); + this.message = createSingleMessage(diagnostic); } private findDialogPath(luFile: LuFile, dialogs: DialogInfo[], d: Diagnostic) { const intentName = luFile.intents.find(intent => { const { range } = intent; if (!range) return false; - return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + return ( + d.range && + isDiagnosticWithInRange(d.range.start.line, d.range?.end.line, range.startLineNumber, range.endLineNumber) + ); })?.Name; return dialogs.find(dialog => dialog.id === luFile.id)?.referredLuIntents.find(lu => lu.name === intentName)?.path; diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index ec02a97bb5..55a68faed8 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -7,13 +7,13 @@ import { LgNamePattern } from '@bfc/shared'; import { StoreContext } from '../../store'; -import { INotification, DialogNotification, LuNotification, LgNotification } from './types'; +import { Notification, DialogNotification, LuNotification, LgNotification } from './types'; import { getReferredFiles } from './../../utils/luUtil'; export default function useNotifications(filter?: string) { const { state } = useContext(StoreContext); const { dialogs, luFiles, lgFiles } = state; const memoized = useMemo(() => { - const notifactions: INotification[] = []; + const notifactions: Notification[] = []; dialogs.forEach(dialog => { dialog.diagnostics.map(diagnostic => { const location = `${dialog.id}.dialog`; @@ -43,20 +43,13 @@ export default function useNotifications(filter?: string) { //should navigate to design page id = `${lgFile.id}#${mappedTemplate.name}`; } - notifactions.push({ - type: 'lg', - severity: DiagnosticSeverity[diagnostic.severity] || '', - location, - message: createSingleMessage(diagnostic), - diagnostic, - id, - }); + notifactions.push(new LgNotification(id, location, diagnostic, dialogs)); }); }); return notifactions; }, [dialogs, luFiles, lgFiles]); - const notifications: INotification[] = !filter ? memoized : memoized.filter(x => x.severity === filter); + const notifications: Notification[] = !filter ? memoized : memoized.filter(x => x.severity === filter); return notifications; } diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index 2d2c81f377..117734326b 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -104,8 +104,10 @@ export function convertPathToUrl(id: string, path?: string): string { return uri; } -export function toUrlUtil(dialogId: string, path: string): string { +export function toUrlUtil(path: string): string { const tokens = path.split('#'); + const firstDotIndex = tokens[0].indexOf('.'); + const dialogId = tokens[0].substring(0, firstDotIndex); const focusedPath = parsePathToFocused(tokens[0]); const selectedPath = parsePathToSelected(tokens[0]); const type = tokens[1]; diff --git a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts index 079ed43c37..7d0e0894df 100644 --- a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts @@ -30,10 +30,22 @@ export function offsetRange(range: Range, offset: number): Range { ); } +export function isDiagnosticWithInRange( + diagnosticStartLine: number, + diagnosticEndLine: number, + sectionStartLine: number, + sectionEndLine: number +): boolean { + return diagnosticStartLine >= sectionStartLine && diagnosticEndLine <= sectionEndLine; +} + export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: { range?: CodeRange }): Diagnostic[] { if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { - return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + return ( + d.range && + isDiagnosticWithInRange(d.range.start.line, d.range.end.line, range.startLineNumber, range.endLineNumber) + ); }); const offset = range.startLineNumber; return filteredDiags.map(d => { @@ -52,7 +64,10 @@ export function filterSectionDiagnostics(diagnostics: Diagnostic[], section: LuI const { range } = section; if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { - return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + return ( + d.range && + isDiagnosticWithInRange(d.range.start.line, d.range.end.line, range.startLineNumber, range.endLineNumber) + ); }); const offset = range.startLineNumber; return filteredDiags.map(d => { From ecd48546227907228e80c2bad4ad43c8c6d6e1ba Mon Sep 17 00:00:00 2001 From: liweitian Date: Wed, 26 Feb 2020 13:20:17 +0800 Subject: [PATCH 14/23] rebase code --- .../language-understanding/code-editor.tsx | 184 ++++++++++++++---- .../client/src/pages/notifications/index.tsx | 4 +- .../packages/lib/indexers/src/luIndexer.ts | 3 +- .../packages/lib/indexers/src/utils/luUtil.ts | 3 +- 4 files changed, 147 insertions(+), 47 deletions(-) diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx index 7c3a2b7553..577b47acdc 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -2,67 +2,167 @@ // Licensed under the MIT License. /* eslint-disable react/display-name */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; import { LuEditor } from '@bfc/code-editor'; import get from 'lodash/get'; import debounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; -import { combineMessage, isValid, LuFile } from '@bfc/indexers'; +import { editor } from '@bfcomposer/monaco-editor/esm/vs/editor/editor.api'; +import { luIndexer, combineMessage, isValid, filterTemplateDiagnostics } from '@bfc/indexers'; import { RouteComponentProps } from '@reach/router'; +import querystring from 'query-string'; + +import { StoreContext } from '../../store'; +import * as luUtil from '../../utils/luUtil'; + +const { parse } = luIndexer; + +const lspServerPath = '/lu-language-server'; interface CodeEditorProps extends RouteComponentProps<{}> { - file: LuFile; - onChange: (value: string) => {}; - errorMsg: string; + fileId: string; } const CodeEditor: React.FC = props => { - const { file, errorMsg: updateErrorMsg } = props; - const onChange = debounce(props.onChange, 500); - const diagnostics = get(file, 'diagnostics', []); - const [content, setContent] = useState(get(file, 'content', '')); + const { actions, state } = useContext(StoreContext); + const { luFiles } = state; + const { fileId } = props; + const file = luFiles?.find(({ id }) => id === fileId); + const [diagnostics, setDiagnostics] = useState(get(file, 'diagnostics', [])); + const [httpErrorMsg, setHttpErrorMsg] = useState(''); + const [luEditor, setLuEditor] = useState(null); + + const search = props.location?.search ?? ''; + const searchSectionName = querystring.parse(search).t; + const sectionId = Array.isArray(searchSectionName) + ? searchSectionName[0] + : typeof searchSectionName === 'string' + ? searchSectionName + : undefined; + const intent = sectionId && file ? file.intents.find(({ Name }) => Name === sectionId) : undefined; + + const hash = props.location?.hash ?? ''; + const hashLine = querystring.parse(hash).L; + const line = Array.isArray(hashLine) ? +hashLine[0] : typeof hashLine === 'string' ? +hashLine : undefined; + + const inlineMode = !!intent; + const [content, setContent] = useState(intent?.Body || file?.content); - const fileId = file && file.id; useEffect(() => { - // reset content with file.content's initial state - if (isEmpty(file)) return; - setContent(file.content); - }, [fileId]); - - // local content maybe invalid and should always sync real-time - // file.content assume to be load from server - const _onChange = value => { + // reset content with file.content initial state + if (!file || isEmpty(file) || content) return; + const value = intent ? intent.Body : file.content; setContent(value); - // TODO: validate before request update server like lg, when luParser is ready - onChange(value); + }, [file, sectionId]); + + const errorMsg = useMemo(() => { + const currentDiagnostics = inlineMode && intent ? filterTemplateDiagnostics(diagnostics, intent) : diagnostics; + const isInvalid = !isValid(currentDiagnostics); + return isInvalid ? combineMessage(diagnostics) : httpErrorMsg; + }, [diagnostics, httpErrorMsg]); + + const editorDidMount = (luEditor: editor.IStandaloneCodeEditor) => { + setLuEditor(luEditor); }; - // diagnostics is load file error, - // updateErrorMsg is save file return error. - const isInvalid = !isValid(file.diagnostics) || updateErrorMsg !== ''; - const errorMsg = isInvalid ? `${combineMessage(diagnostics)}\n ${updateErrorMsg}` : ''; + useEffect(() => { + if (luEditor && line !== undefined) { + window.requestAnimationFrame(() => { + luEditor.revealLine(line); + luEditor.focus(); + luEditor.setPosition({ lineNumber: line, column: 1 }); + }); + } + }, [line, luEditor]); + + const updateLuIntent = useMemo( + () => + debounce((Body: string) => { + if (!file || !intent) return; + const { Name } = intent; + const payload = { + file, + intentName: Name, + intent: { + Name, + Body, + }, + }; + actions.updateLuIntent(payload); + }, 500), + [file, intent] + ); + + const updateLuFile = useMemo( + () => + debounce((content: string) => { + if (!file) return; + const { id } = file; + const payload = { + id, + content, + }; + actions.updateLuFile(payload); + }, 500), + [file] + ); + + const updateDiagnostics = useMemo( + () => + debounce((value: string) => { + if (!file) return; + const { id } = file; + if (inlineMode) { + if (!intent) return; + const { Name } = intent; + const { content } = file; + try { + const newContent = luUtil.updateIntent(content, Name, { + Name, + Body: value, + }); + const { diagnostics } = parse(newContent, id); + setDiagnostics(diagnostics); + } catch (error) { + setHttpErrorMsg(error.error); + } + } else { + const { diagnostics } = parse(value, id); + setDiagnostics(diagnostics); + } + }, 1000), + [file, intent] + ); + + const _onChange = useCallback( + value => { + setContent(value); + updateDiagnostics(value); + if (!file) return; + if (inlineMode) { + updateLuIntent(value); + } else { + updateLuFile(value); + } + }, + [file, intent] + ); + + const luOption = { + fileId, + sectionId: intent?.Name, + }; return ( ); diff --git a/Composer/packages/client/src/pages/notifications/index.tsx b/Composer/packages/client/src/pages/notifications/index.tsx index ea4644eda6..b4ba386262 100644 --- a/Composer/packages/client/src/pages/notifications/index.tsx +++ b/Composer/packages/client/src/pages/notifications/index.tsx @@ -3,11 +3,9 @@ /** @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'; diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 49a579bcc0..164ea2d89c 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -3,8 +3,9 @@ import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; import get from 'lodash/get'; +import { LuIntentSection } from '@bfc/shared'; -import { FileInfo, LuFile, LuParsed, LuSectionTypes, LuIntentSection } from './type'; +import { FileInfo, LuFile, LuParsed, LuSectionTypes } from './type'; import { getBaseName } from './utils/help'; import { Diagnostic, Position, Range, DiagnosticSeverity } from './diagnostic'; import { FileExtensions } from './utils/fileExtensions'; diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index ef0867686d..0baac955ff 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -9,8 +9,9 @@ import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; import isEmpty from 'lodash/isEmpty'; +import { LuIntentSection } from '@bfc/shared'; -import { LuIntentSection, LuSectionTypes } from '../type'; +import { LuSectionTypes } from '../type'; import { luIndexer } from '../luIndexer'; import { Diagnostic } from '../diagnostic'; const { parse } = luIndexer; From 98f01e690aac2ded2b98308038e5fe3df6f9f960 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Feb 2020 13:50:48 +0800 Subject: [PATCH 15/23] resolve rebase conflict --- .../lib/indexers/__tests__/luUtil.test.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts index 5eaa009cff..916061b82d 100644 --- a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts @@ -90,13 +90,14 @@ describe('LU Section CRUD test', () => { Name: 'CheckEmail', Body: `- check my email - show my emails -@`, +`, }; const invalidFileContent = `#CheckEmail - - check my email - - show my emails - @`; +- check my email +- show my emails +@`; + const invalidIntent = { Name: 'CheckEmail', Body: `- check my email @@ -104,10 +105,20 @@ describe('LU Section CRUD test', () => { @`, }; + const invalidIntent4 = { + Name: 'CheckEmail', + Body: `- check my email +- show my emails + +# UnexpectedIntentDefination +- unexpected intent body +`, + }; // intent invalid const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); expect(updatedContent2).toEqual(validFileContent); - + const updatedContent4 = updateIntent(validFileContent, intentName, invalidIntent4); + expect(updatedContent4).toEqual(validFileContent); // file invalid const updatedContent3 = updateIntent(invalidFileContent, intentName, validIntent); expect(updatedContent3).toEqual(invalidFileContent); From 6f80aa883b7d236f1a56bbe7e074c6d814372f66 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Feb 2020 14:25:20 +0800 Subject: [PATCH 16/23] enable inline mode --- .../ProjectTree/TriggerCreationModal.tsx | 4 ++++ .../fields/RecognizerField/InlineLuEditor.tsx | 8 ++++++-- .../src/Form/widgets/LuEditorWidget.tsx | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 6e6675c6dc..4b5019a892 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -222,6 +222,10 @@ export const TriggerCreationModal: React.FC = props = value={formData.triggerPhrases} errorMsg={formData.errors.triggerPhrases} hidePlaceholder={true} + luOption={{ + fileId: dialogId, + sectionId: formData.intent || 'newSection', + }} height={150} /> )} diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx index 65f989aabf..2652028a8e 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx @@ -20,8 +20,12 @@ const InlineLuEditor: React.FC = props => { setLocalContent(value); onSave(value); }; - - return ; + const luOption = { + fileId: file.id, + }; + return ( + + ); }; export default InlineLuEditor; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index 0f019352ac..3b4e5be921 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -67,7 +67,21 @@ export const LuEditorWidget: React.FC = props => { } }, []); - return ; + const luOption = { + fileId: luFileId, + sectionId: luIntent?.Name, + }; + + return ( + + ); }; export default LuEditorWidget; From 7db9e31d1c95953945d0b1dbad5f9fea7aabd0ca Mon Sep 17 00:00:00 2001 From: Long Alan Date: Wed, 26 Feb 2020 23:41:12 +0800 Subject: [PATCH 17/23] refactor luEditorWidget --- .../src/Form/widgets/IntentWidget.tsx | 2 +- .../src/Form/widgets/LuEditorWidget.tsx | 129 ++++++++++-------- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx index 8d083dd329..bc3579bb6d 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx @@ -10,7 +10,7 @@ import { DialogInfo } from '@bfc/indexers'; import { BFDWidgetProps } from '../types'; import { WidgetLabel } from './WidgetLabel'; -import LuEditorWidget from './LuEditorWidget'; +import { LuEditorWidget } from './LuEditorWidget'; const EMPTY_OPTION = { key: '', text: '' }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index 3b4e5be921..dc054daa57 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState, useMemo, useEffect } from 'react'; +import React from 'react'; import { LuEditor } from '@bfc/code-editor'; import debounce from 'lodash/debounce'; import { LuIntentSection } from '@bfc/shared'; @@ -16,72 +16,81 @@ interface LuEditorWidgetProps { onChange: (template?: string) => void; } -export const LuEditorWidget: React.FC = props => { - const { formContext, name, height = 250 } = props; - const luFileId = formContext.currentDialog.id; - const luFile: LuFile | null = formContext.luFiles.find(f => f.id === luFileId); - const luIntent: LuIntentSection = (luFile && luFile.intents.find(intent => intent.Name === name)) || { - Name: name, - Body: '', - }; - - const updateLuIntent = useMemo( - () => - debounce((body: string) => { - formContext.shellApi.updateLuIntent(luFileId, name, { Name: name, Body: body }).catch(() => {}); - }, 500), - [name, luFileId] - ); - - const diagnostic = luFile && filterSectionDiagnostics(luFile.diagnostics, luIntent)[0]; - - const errorMsg = diagnostic - ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] - : ''; +export class LuEditorWidget extends React.Component { + constructor(props) { + super(props); + this.debounceUpdate = debounce(this.updateLuIntent, 500); + this.name = this.props.name; + this.formContext = this.props.formContext; + this.luFileId = this.formContext.currentDialog.id; + this.luFile = this.formContext.luFiles.find(f => f.id === this.luFileId); + this.luIntent = (this.luFile && this.luFile.intents.find(intent => intent.Name === this.name)) || { + Name: this.name, + Body: '', + }; + } - const [localValue, setLocalValue] = useState(luIntent.Body); + formContext: FormContext; + name: string; + luFileId: string; + luFile: LuFile | null; + luIntent: LuIntentSection; + state = { localValue: '' }; + debounceUpdate; + updateLuIntent = (body: string) => { + this.formContext.shellApi.updateLuIntent(this.luFileId, this.name, { Name: this.name, Body: body }).catch(() => {}); + }; - // updating localValue when getting newest luIntent Data - // it will be deleted after leilei's pr: fix: Undo / redo behavior on LG resources - useEffect(() => { - if (!localValue) { - setLocalValue(luIntent.Body); + static getDerivedStateFromProps(nextProps, prevState) { + const name = nextProps.name; + const formContext = nextProps.formContext; + const luFileId = formContext.currentDialog.id; + const luFile = formContext.luFiles.find(f => f.id === luFileId); + const luIntent = (luFile && luFile.intents.find(intent => intent.Name === name)) || { + Name: name, + Body: '', + }; + if (prevState !== luIntent.Body) { + return { + localValue: luIntent.Body, + }; } - }, [luIntent.Body]); - const onChange = (body: string) => { - setLocalValue(body); - if (luFileId) { + return null; + } + + onChange = (body: string) => { + this.setState({ + localValue: body, + }); + if (this.luFileId) { if (body) { - updateLuIntent(body); + this.updateLuIntent(body); } else { - updateLuIntent.flush(); - formContext.shellApi.removeLuIntent(luFileId, name); + this.formContext.shellApi.removeLuIntent(this.luFileId, this.name); } } }; + render() { + const diagnostic = this.luFile && filterSectionDiagnostics(this.luFile.diagnostics, this.luIntent)[0]; - // update the template on mount to get validation - useEffect(() => { - if (localValue) { - updateLuIntent(localValue); - } - }, []); - - const luOption = { - fileId: luFileId, - sectionId: luIntent?.Name, - }; - - return ( - - ); -}; + const errorMsg = diagnostic + ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] + : ''; + const luOption = { + fileId: this.luFileId, + sectionId: this.luIntent?.Name, + }; + const height = this.props.height || 250; -export default LuEditorWidget; + return ( + + ); + } +} From 18aa11783bfdf541b2935857649b5e3c9160199f Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Feb 2020 16:34:45 +0800 Subject: [PATCH 18/23] replace third-part npm registry --- Composer/yarn.lock | 54 +++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 160659b32a..be76d64dd2 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -1972,6 +1972,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.2.0", "@babel/runtime@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": version "7.4.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8" @@ -2014,13 +2021,6 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" - integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== - dependencies: - regenerator-runtime "^0.13.2" - "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" @@ -8554,7 +8554,7 @@ findup-sync@3.0.0: findup-sync@^2.0.0: version "2.0.0" - resolved "https://registry.npm.taobao.org/findup-sync/download/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= dependencies: detect-file "^1.0.0" @@ -8869,6 +8869,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-node-dimensions@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" + integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" @@ -9561,7 +9566,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1: +https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: version "2.2.4" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha1-TuenN6vZJniik9mzShr00NCMeHs= @@ -14474,6 +14479,16 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-measure@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e" + integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A== + dependencies: + "@babel/runtime" "^7.2.0" + get-node-dimensions "^1.2.1" + prop-types "^15.6.2" + resize-observer-polyfill "^1.5.0" + react-testing-library@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.2.tgz#afd7ddaa174e21cf672605e4e4f6f8156c4c9ef9" @@ -14622,8 +14637,8 @@ realpath-native@^1.1.0: reconnecting-websocket@^3.2.2: version "3.2.2" - resolved "https://registry.npm.taobao.org/reconnecting-websocket/download/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee" - integrity sha1-gJdRTpJumFXgPDnnbvouPR83G+4= + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee" + integrity sha512-SWSfoXiaHVOqXuPWFgGWeUxKnb5HIY7I/Fh5C/hy4wUOgeOh7YIMXEiv5/eHBlNs4tNzCrO5YDR9AH62NWle0Q== recursive-readdir@2.2.2: version "2.2.2" @@ -14834,13 +14849,13 @@ replace-ext@1.0.0: integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= request-light@^0.2.2: - version "0.2.4" - resolved "https://registry.npm.taobao.org/request-light/download/request-light-0.2.4.tgz#3cea29c126682e6bcadf7915353322eeba01a755" - integrity sha1-POopwSZoLmvK33kVNTMi7roBp1U= + version "0.2.5" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.5.tgz#38a3da7b2e56f7af8cbba57e8a94930ee2380746" + integrity sha512-eBEh+GzJAftUnex6tcL6eV2JCifY0+sZMIUpUPOVXbs2nV5hla4ZMmO3icYKGuGVuQ2zHE9evh4OrRcH4iyYYw== dependencies: http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - vscode-nls "^4.0.0" + https-proxy-agent "^2.2.3" + vscode-nls "^4.1.1" request-progress@3.0.0: version "3.0.0" @@ -14926,6 +14941,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -17102,7 +17122,7 @@ vscode-languageserver@^5.3.0-next: vscode-languageserver-protocol "^3.15.0-next.8" vscode-textbuffer "^1.0.0" -vscode-nls@^4.0.0, vscode-nls@^4.1.1: +vscode-nls@^4.1.1: version "4.1.1" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" integrity sha1-+ZFrZOSUeyAyLe+x5naklYYfEzw= From ba98fe89e3f39aba4002a8e3a6a7b070d017587e Mon Sep 17 00:00:00 2001 From: liweitian Date: Wed, 26 Feb 2020 21:56:55 +0800 Subject: [PATCH 19/23] update LgNotification class --- .../client/src/pages/notifications/index.tsx | 2 +- .../client/src/pages/notifications/types.ts | 15 ++++++--------- .../src/pages/notifications/useNotifications.tsx | 7 ++++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Composer/packages/client/src/pages/notifications/index.tsx b/Composer/packages/client/src/pages/notifications/index.tsx index b4ba386262..093238013a 100644 --- a/Composer/packages/client/src/pages/notifications/index.tsx +++ b/Composer/packages/client/src/pages/notifications/index.tsx @@ -20,7 +20,7 @@ const Notifications: React.FC = () => { const notifications = useNotifications(filter); const navigations = { [NotificationType.LG]: (item: INotification) => { - let url = `/language-generation/common/edit#L=${item.diagnostic.range?.start.line || 0}`; + let url = `/language-generation/${item.id}/edit#L=${item.diagnostic.range?.start.line || 0}`; //the format of item.id is lgFile#inlineTemplateId if (item.dialogPath) { url = toUrlUtil(item.dialogPath); diff --git a/Composer/packages/client/src/pages/notifications/types.ts b/Composer/packages/client/src/pages/notifications/types.ts index 957cd17aa5..f61d3f3a04 100644 --- a/Composer/packages/client/src/pages/notifications/types.ts +++ b/Composer/packages/client/src/pages/notifications/types.ts @@ -51,18 +51,15 @@ export class DialogNotification extends Notification { export class LgNotification extends Notification { type = NotificationType.LG; dialogPath?: string; - constructor(id: string, location: string, diagnostic: Diagnostic, dialogs: DialogInfo[]) { + constructor(id: string, lgTemplateName: string, location: string, diagnostic: Diagnostic, dialogs: DialogInfo[]) { super(id, location, diagnostic); this.message = createSingleMessage(diagnostic); - this.dialogPath = this.findDialogPath(dialogs, id); + this.dialogPath = this.findDialogPath(dialogs, id, lgTemplateName); } - private findDialogPath(dialogs: DialogInfo[], id: string) { - const dividerIndex = id.indexOf('#'); - if (dividerIndex > -1) { - const templateId = id.substring(dividerIndex + 1); - const lgFile = id.substring(0, dividerIndex); - const dialog = dialogs.find(d => d.lgFile === lgFile); - const lgTemplate = dialog ? dialog.lgTemplates.find(lg => lg.name === templateId) : null; + private findDialogPath(dialogs: DialogInfo[], id: string, lgTemplateName: string) { + if (lgTemplateName) { + const dialog = dialogs.find(d => d.lgFile === id); + const lgTemplate = dialog ? dialog.lgTemplates.find(lg => lg.name === lgTemplateName) : null; const path = lgTemplate ? lgTemplate.path : ''; return path; } diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 55a68faed8..f926cd1788 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -37,13 +37,14 @@ export default function useNotifications(filter?: string) { get(diagnostic, 'range.start.line') >= get(t, 'range.startLineNumber') && get(diagnostic, 'range.end.line') <= get(t, 'range.endLineNumber') ); - let id = lgFile.id; + const id = lgFile.id; const location = `${lgFile.id}.lg`; + let lgTemplateName = ''; if (mappedTemplate && mappedTemplate.name.match(LgNamePattern)) { //should navigate to design page - id = `${lgFile.id}#${mappedTemplate.name}`; + lgTemplateName = mappedTemplate.name; } - notifactions.push(new LgNotification(id, location, diagnostic, dialogs)); + notifactions.push(new LgNotification(id, lgTemplateName, location, diagnostic, dialogs)); }); }); return notifactions; From fe46660689b94b1056e37f7ef438b2d477745e04 Mon Sep 17 00:00:00 2001 From: liweitian Date: Fri, 28 Feb 2020 19:42:06 +0800 Subject: [PATCH 20/23] handle comments --- .../__tests__/components/design.test.js | 9 +- .../__mocks__/TriggerCreationModal.tsx | 217 ------------------ .../language-understanding/table-view.tsx | 2 +- .../client/src/pages/notifications/types.ts | 7 +- .../pages/notifications/useNotifications.tsx | 2 +- .../lib/indexers/src/utils/diagnosticUtil.ts | 20 +- 6 files changed, 16 insertions(+), 241 deletions(-) delete mode 100644 Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx diff --git a/Composer/packages/client/__tests__/components/design.test.js b/Composer/packages/client/__tests__/components/design.test.js index a46d188f2d..f5bed60f75 100644 --- a/Composer/packages/client/__tests__/components/design.test.js +++ b/Composer/packages/client/__tests__/components/design.test.js @@ -10,7 +10,14 @@ 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('./../../src/components/ProjectTree/TriggerCreationModal'); + +//jest.mock('./../../src/components/ProjectTree/TriggerCreationModal'); +jest.mock('@bfc/code-editor', () => { + return { + LuEditor: () =>
, + }; +}); + describe('', () => { it('should render the ProjectTree', async () => { const dialogId = 'Main'; diff --git a/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx deleted file mode 100644 index 16cf31b36f..0000000000 --- a/Composer/packages/client/src/components/ProjectTree/__mocks__/TriggerCreationModal.tsx +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -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 } from '@bfc/indexers'; -import get from 'lodash/get'; - -import { - addNewTrigger, - getTriggerTypes, - TriggerFormData, - TriggerFormDataErrors, - eventTypeKey, - intentTypeKey, - activityTypeKey, - messageTypeKey, - getEventTypes, - getActivityTypes, - getMessageTypes, -} from '../../../utils/dialogUtil'; -import { addIntent } from '../../../utils/luUtil'; -import { StoreContext } from '../../../store'; -import { styles, dropdownStyles, dialogWindow, intent } from '../styles'; - -const nameRegex = /^[a-zA-Z0-9-_.]+$/; -const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { - const errors: TriggerFormDataErrors = {}; - const { $type, specifiedType, intent, triggerPhrases } = data; - - if ($type === eventTypeKey && !specifiedType) { - errors.specifiedType = formatMessage('Please select a event type'); - } - - if ($type === activityTypeKey && !specifiedType) { - errors.specifiedType = formatMessage('Please select an activity type'); - } - - 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, luFilePayload: LuFilePayload) => void; -} - -const initialFormData: TriggerFormData = { - errors: {}, - $type: intentTypeKey, - specifiedType: '', - intent: '', - triggerPhrases: '', -}; - -const triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); - -export const TriggerCreationModal: React.FC = props => { - const { isOpen, onDismiss, onSubmit, dialogId } = props; - const [formData, setFormData] = useState(initialFormData); - const { state } = useContext(StoreContext); - const { dialogs, luFiles } = state; - const luFile = luFiles.find(lu => lu.id === dialogId); - - const onClickSubmitButton = e => { - e.preventDefault(); - const errors = validateForm(formData); - - if (Object.keys(errors).length) { - setFormData({ - ...formData, - errors, - }); - 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, updateLuFile); - onDismiss(); - }; - - const onSelectTriggerType = (e, option) => { - setFormData({ ...initialFormData, $type: option.key }); - }; - - const onSelectSpecifiedTypeType = (e, option) => { - setFormData({ ...formData, specifiedType: option.key }); - }; - - const onNameChange = (e, name) => { - setFormData({ ...formData, intent: name }); - }; - - const eventTypes: IDropdownOption[] = getEventTypes(); - const activityTypes: IDropdownOption[] = getActivityTypes(); - const messageTypes: IDropdownOption[] = getMessageTypes(); - - const showIntentFields = formData.$type === intentTypeKey; - const showEventDropDown = formData.$type === eventTypeKey; - const showActivityDropDown = formData.$type === activityTypeKey; - const showMessageDropDown = formData.$type === messageTypeKey; - - return ( - - ); -}; diff --git a/Composer/packages/client/src/pages/language-understanding/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index 8840ecfba7..2edbf09e13 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -79,7 +79,7 @@ const TableView: React.FC = props => { name, phrases, fileId: luFile.id, - used: luDialog ? !!luDialog.referredLuIntents.find(lu => lu.name === 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, }); }); diff --git a/Composer/packages/client/src/pages/notifications/types.ts b/Composer/packages/client/src/pages/notifications/types.ts index f61d3f3a04..707e3823da 100644 --- a/Composer/packages/client/src/pages/notifications/types.ts +++ b/Composer/packages/client/src/pages/notifications/types.ts @@ -40,7 +40,6 @@ export abstract class Notification implements INotification { export class DialogNotification extends Notification { type = NotificationType.DIALOG; - dialogPath?: string; constructor(id: string, location: string, diagnostic: Diagnostic) { super(id, location, diagnostic); this.message = `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`; @@ -50,7 +49,6 @@ export class DialogNotification extends Notification { export class LgNotification extends Notification { type = NotificationType.LG; - dialogPath?: string; constructor(id: string, lgTemplateName: string, location: string, diagnostic: Diagnostic, dialogs: DialogInfo[]) { super(id, location, diagnostic); this.message = createSingleMessage(diagnostic); @@ -78,10 +76,7 @@ export class LuNotification extends Notification { const intentName = luFile.intents.find(intent => { const { range } = intent; if (!range) return false; - return ( - d.range && - isDiagnosticWithInRange(d.range.start.line, d.range?.end.line, range.startLineNumber, range.endLineNumber) - ); + return isDiagnosticWithInRange(d, range); })?.Name; return dialogs.find(dialog => dialog.id === luFile.id)?.referredLuIntents.find(lu => lu.name === intentName)?.path; diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index f926cd1788..91441951de 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -50,7 +50,7 @@ export default function useNotifications(filter?: string) { return notifactions; }, [dialogs, luFiles, lgFiles]); - const notifications: Notification[] = !filter ? memoized : memoized.filter(x => x.severity === filter); + const notifications: Notification[] = filter ? memoized.filter(x => x.severity === filter) : memoized; return notifications; } diff --git a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts index 7d0e0894df..92c16061f3 100644 --- a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts @@ -30,22 +30,15 @@ export function offsetRange(range: Range, offset: number): Range { ); } -export function isDiagnosticWithInRange( - diagnosticStartLine: number, - diagnosticEndLine: number, - sectionStartLine: number, - sectionEndLine: number -): boolean { - return diagnosticStartLine >= sectionStartLine && diagnosticEndLine <= sectionEndLine; +export function isDiagnosticWithInRange(diagnostic: Diagnostic, range: CodeRange): boolean { + if (!diagnostic.range) return false; + return diagnostic.range.start.line >= range.startLineNumber && diagnostic.range.end.line <= range.endLineNumber; } export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: { range?: CodeRange }): Diagnostic[] { if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { - return ( - d.range && - isDiagnosticWithInRange(d.range.start.line, d.range.end.line, range.startLineNumber, range.endLineNumber) - ); + return d.range && isDiagnosticWithInRange(d, range); }); const offset = range.startLineNumber; return filteredDiags.map(d => { @@ -64,10 +57,7 @@ export function filterSectionDiagnostics(diagnostics: Diagnostic[], section: LuI const { range } = section; if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { - return ( - d.range && - isDiagnosticWithInRange(d.range.start.line, d.range.end.line, range.startLineNumber, range.endLineNumber) - ); + return isDiagnosticWithInRange(d, range); }); const offset = range.startLineNumber; return filteredDiags.map(d => { From 95a81066eda2a98b0e35a9833c2617a454e1c7d6 Mon Sep 17 00:00:00 2001 From: liweitian Date: Fri, 28 Feb 2020 19:43:14 +0800 Subject: [PATCH 21/23] handle comments --- Composer/packages/client/__tests__/components/design.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Composer/packages/client/__tests__/components/design.test.js b/Composer/packages/client/__tests__/components/design.test.js index f5bed60f75..dbd3ce46fa 100644 --- a/Composer/packages/client/__tests__/components/design.test.js +++ b/Composer/packages/client/__tests__/components/design.test.js @@ -11,7 +11,6 @@ import { TriggerCreationModal } from './../../src/components/ProjectTree/Trigger import { ProjectTree } from './../../src/components/ProjectTree'; import { CreateDialogModal } from './../../src/pages/design/createDialogModal'; -//jest.mock('./../../src/components/ProjectTree/TriggerCreationModal'); jest.mock('@bfc/code-editor', () => { return { LuEditor: () =>
, From 7663a87f414d2cd2ced7f36e268a330714bd0ee0 Mon Sep 17 00:00:00 2001 From: Long Alan Date: Sun, 1 Mar 2020 22:14:26 +0800 Subject: [PATCH 22/23] fix lu editor widget --- .../obiformeditor/src/Form/widgets/LuEditorWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index dc054daa57..d0f5921570 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -50,7 +50,7 @@ export class LuEditorWidget extends React.Component { Name: name, Body: '', }; - if (prevState !== luIntent.Body) { + if (!prevState.localValue) { return { localValue: luIntent.Body, }; From 980b031231200355aba54d62b2b246f7f315eb9a Mon Sep 17 00:00:00 2001 From: Chris Whitten Date: Sun, 1 Mar 2020 08:40:43 -0800 Subject: [PATCH 23/23] Configure Inline LU Editor to match Inline LG Editor --- .../components/ProjectTree/TriggerCreationModal.tsx | 11 +++++++++++ Composer/packages/client/src/pages/design/styles.js | 4 ++-- .../obiformeditor/src/Form/widgets/LuEditorWidget.tsx | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 4b5019a892..472df83534 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -226,6 +226,17 @@ export const TriggerCreationModal: React.FC = props = fileId: dialogId, sectionId: formData.intent || 'newSection', }} + options={{ + lineNumbers: 'off', + minimap: { + enabled: false, + }, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + glyphMargin: false, + folding: false, + renderLineHighlight: 'none', + }} height={150} /> )} diff --git a/Composer/packages/client/src/pages/design/styles.js b/Composer/packages/client/src/pages/design/styles.js index 5b267a00eb..a155b225fd 100644 --- a/Composer/packages/client/src/pages/design/styles.js +++ b/Composer/packages/client/src/pages/design/styles.js @@ -113,9 +113,9 @@ export const middleTriggerContainer = css` display: flex; justify-content: center; align-items: center; - background: #e5e5e5; + background: #f6f6f6; width: 100%; - margin-top: 48px; + margin-top: 55px; height: calc(100% - 48px); position: absolute; `; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index d0f5921570..0d05aac956 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -89,6 +89,17 @@ export class LuEditorWidget extends React.Component { errorMsg={errorMsg} hidePlaceholder={true} luOption={luOption} + options={{ + lineNumbers: 'off', + minimap: { + enabled: false, + }, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + glyphMargin: false, + folding: false, + renderLineHighlight: 'none', + }} height={height} /> );