diff --git a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx index b5a7223749..47de132793 100644 --- a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx +++ b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx @@ -18,6 +18,8 @@ const luisConfig = { defaultLanguage: 'en-us', environment: 'composer', }; +const config = { subscriptionKey: '12345', ...luisConfig }; +const qnaConfig = { subscriptionKey: '12345', endpointKey: '12345' }; describe('', () => { it('should render the ', () => { const onDismiss = jest.fn(() => {}); @@ -31,10 +33,11 @@ describe('', () => { set(botNameState, 'sampleBot0'); set(settingsState, { luis: luisConfig, + qna: qnaConfig, }); }; const { getByText } = renderWithRecoil( - , + , recoilInitState ); diff --git a/Composer/packages/client/__tests__/utils/luUtil.test.ts b/Composer/packages/client/__tests__/utils/luUtil.test.ts index ec64b7cf38..4ac9177823 100644 --- a/Composer/packages/client/__tests__/utils/luUtil.test.ts +++ b/Composer/packages/client/__tests__/utils/luUtil.test.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LuFile, DialogInfo, Diagnostic, DiagnosticSeverity } from '@bfc/shared'; +import { LuFile, DialogInfo } from '@bfc/shared'; -import { getReferredLuFiles, checkLuisPublish, createCrossTrainConfig } from '../../src/utils/luUtil'; +import { getReferredLuFiles, createCrossTrainConfig } from '../../src/utils/luUtil'; describe('getReferredLuFiles', () => { it('returns referred luFiles from dialog', () => { @@ -93,34 +93,4 @@ describe('getReferredLuFiles', () => { expect(config.triggerRules['dia1.en-us.lu']['dia4.en-us.lu']).toBeUndefined(); expect(config.triggerRules['main.en-us.lu'].dialog_without_lu).toEqual(''); }); - - it('check the lu files before publish', () => { - const dialogs = [{ luFile: 'a' }] as DialogInfo[]; - const diagnostics: Diagnostic[] = []; - const luFiles = [ - { id: 'a.en-us', diagnostics, content: 'test', intents: [{ Name: '1', Body: '1' }], empty: false }, - { id: 'b.en-us', diagnostics }, - { id: 'c.en-us', diagnostics }, - ] as LuFile[]; - const referred = checkLuisPublish(luFiles, dialogs); - expect(referred.length).toEqual(1); - - expect(referred[0].id).toEqual('a.en-us'); - - luFiles[0].diagnostics = [{ message: 'wrong', severity: DiagnosticSeverity.Error }] as Diagnostic[]; - expect(() => { - checkLuisPublish(luFiles, dialogs); - }).toThrowError(/wrong/); - - luFiles[0].diagnostics = []; - luFiles[0].intents = []; - luFiles[0].empty = true; - expect(() => { - checkLuisPublish(luFiles, dialogs); - }).toThrowError('You have the following empty LuFile(s): a.en-us'); - - luFiles[0].empty = false; - - expect(checkLuisPublish(luFiles, dialogs)[0].id).toEqual('a.en-us'); - }); }); diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 070ef5d447..717beca90a 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -33,7 +33,6 @@ "@bfc/ui-plugin-select-dialog": "*", "@bfc/ui-plugin-select-skill-dialog": "*", "@emotion/core": "^10.0.27", - "@microsoft/bf-lu": "^4.10.0-preview.141651", "@reach/router": "^1.2.1", "@uifabric/fluent-theme": "^7.1.107", "@uifabric/icons": "^7.3.59", @@ -41,7 +40,6 @@ "@uifabric/styling": "^7.13.7", "axios": "^0.19.2", "babel-plugin-extract-format-message": "^6.2.3", - "botbuilder-lg": "4.10.0-preview-147186", "format-message": "^6.2.3", "immer": "^5.2.0", "jwt-decode": "^2.2.0", @@ -57,8 +55,8 @@ "react-frame-component": "^4.0.2", "react-timeago": "^4.4.0", "recoil": "^0.0.10", - "webpack-bundle-analyzer": "^3.8.0", - "styled-components": "^4.1.3" + "styled-components": "^4.1.3", + "webpack-bundle-analyzer": "^3.8.0" }, "browserslist": [ ">0.2%", @@ -67,7 +65,6 @@ "not op_mini all" ], "devDependencies": { - "@types/recoil": "^0.0.1", "@babel/cli": "7.2.3", "@babel/core": "7.3.4", "@babel/runtime": "7.3.4", @@ -79,6 +76,7 @@ "@types/reach__router": "^1.2.4", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", + "@types/recoil": "^0.0.1", "@types/webpack-env": "^1.15.2", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.1.0", diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 18bd2e634c..5b8094e15d 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -14,14 +14,14 @@ import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { luIndexer, combineMessage } from '@bfc/indexers'; import { PlaceHolderSectionName } from '@bfc/indexers/lib/utils/luUtil'; -import { DialogInfo, SDKKinds } from '@bfc/shared'; +import { DialogInfo, SDKKinds, LuIntentSection } from '@bfc/shared'; import { LuEditor, inlineModePlaceholder } from '@bfc/code-editor'; import { IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox'; import { useRecoilValue } from 'recoil'; import { FontWeights } from '@uifabric/styling'; import { FontSizes } from '@uifabric/fluent-theme'; import get from 'lodash/get'; -import { generateUniqueId } from '@bfc/shared'; +import { generateDesignerId, LgTemplate } from '@bfc/shared'; import { generateNewDialog, @@ -38,16 +38,8 @@ import { qnaMatcherKey, onChooseIntentKey, } from '../../utils/dialogUtil'; -import { addIntent } from '../../utils/luUtil'; -import { addTemplate } from '../../utils/lgUtil'; -import { - dialogsState, - luFilesState, - lgFilesState, - localeState, - projectIdState, - schemasState, -} from '../../recoilModel/atoms/botState'; + +import { dialogsState, projectIdState, schemasState } from '../../recoilModel/atoms/botState'; import { userSettingsState } from '../../recoilModel'; import { nameRegex } from '../../constants'; @@ -224,25 +216,15 @@ interface TriggerCreationModalProps { dialogId: string; isOpen: boolean; onDismiss: () => void; - onSubmit: ( - dialog: DialogInfo, - luFilePayload?: LuFilePayload, - lgFilePayload?: LgFilePayload, - QnAFilePayload?: QnAFilePayload - ) => void; + onSubmit: (dialog: DialogInfo, intent?: LuIntentSection, lgTemplate?: LgTemplate[]) => void; } export const TriggerCreationModal: React.FC = (props) => { const { isOpen, onDismiss, onSubmit, dialogId } = props; const dialogs = useRecoilValue(dialogsState); - const luFiles = useRecoilValue(luFilesState); - const lgFiles = useRecoilValue(lgFilesState); - const locale = useRecoilValue(localeState); const projectId = useRecoilValue(projectIdState); const schemas = useRecoilValue(schemasState); const userSettings = useRecoilValue(userSettingsState); - const luFile = luFiles.find(({ id }) => id === `${dialogId}.${locale}`); - const lgFile = lgFiles.find(({ id }) => id === `${dialogId}.${locale}`); const dialogFile = dialogs.find((dialog) => dialog.id === dialogId); const isRegEx = (dialogFile?.content?.recognizer?.$kind ?? '') === regexRecognizerKey; const recognizer = get(dialogFile, 'content.recognizer', ''); @@ -303,39 +285,31 @@ export const TriggerCreationModal: React.FC = (props) if (formData.$kind === intentTypeKey && isLUISnQnA && formData.triggerPhrases) { const newDialog = generateNewDialog(dialogs, dialogId, formData, schemas.sdk?.content, {}); - const content = luFile?.content ?? ''; - const luFileId = luFile?.id || `${dialogId}.${locale}`; - const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases }); - const updateLuFile = { - id: luFileId, - content: newContent, - }; - onSubmit(newDialog, updateLuFile); + const newIntent = { Name: formData.intent, Body: formData.triggerPhrases }; + onSubmit(newDialog, newIntent); } else if (formData.$kind === qnaMatcherKey || formData.$kind === onChooseIntentKey) { - const lgTemplateId = generateUniqueId(6); - const extraTriggerAttributes = { lgTemplateId }; - const newDialog = generateNewDialog(dialogs, dialogId, formData, schemas.sdk?.content, extraTriggerAttributes); - const content = lgFile?.content ?? ''; - const lgFileId = lgFile?.id || `common.${locale}`; - let body = ''; - let name = ''; - if (formData.$kind === qnaMatcherKey) { - name = `SendActivity_${lgTemplateId}`; - body = '- ${@answer}'; - } else if (formData.$kind === onChooseIntentKey) { - name = `ChoiceInput_Prompt_${lgTemplateId}`; - body = '- Multiple intents detected, please choose which intent do you want to trigger?'; + const lgTemplateId1 = generateDesignerId(); + const lgTemplateId2 = generateDesignerId(); + let extraTriggerAttributes = {}; + let lgTemplates: LgTemplate[] = []; + if (formData.$kind === onChooseIntentKey) { + extraTriggerAttributes['actions[4].prompt'] = `\${TextInput_Prompt_${lgTemplateId1}()}`; + extraTriggerAttributes['actions[5].elseActions[0].activity'] = `\${SendActivity_${lgTemplateId2}()}`; + lgTemplates = [ + { + name: `TextInput_Prompt_${lgTemplateId1}`, + body: `[Activity\n + Attachments = \${json(AdaptiveCardJson())}\n + ]\n`, + } as LgTemplate, + { + name: `SendActivity_${lgTemplateId2}`, + body: '- Sure, no worries.', + } as LgTemplate, + ]; } - const newLgFile = addTemplate(lgFileId, content, { - name, - parameters: [], - body, - }); - const updateLgFile = { - id: newLgFile.id, - content: newLgFile.content, - }; - onSubmit(newDialog, undefined, updateLgFile); + const newDialog = generateNewDialog(dialogs, dialogId, formData, schemas.sdk?.content, extraTriggerAttributes); + onSubmit(newDialog, undefined, lgTemplates); } else { const newDialog = generateNewDialog(dialogs, dialogId, formData, schemas.sdk?.content, {}); onSubmit(newDialog); @@ -351,7 +325,10 @@ export const TriggerCreationModal: React.FC = (props) if (isCompound) { newFormData = { ...newFormData, $kind: '' }; } else { - newFormData = { ...newFormData, $kind: option.key === customEventKey ? SDKKinds.OnDialogEvent : option.key }; + newFormData = { + ...newFormData, + $kind: option.key === customEventKey ? SDKKinds.OnDialogEvent : option.key, + }; } setFormData({ ...newFormData, errors: initialFormDataErrors }); }; @@ -371,7 +348,11 @@ export const TriggerCreationModal: React.FC = (props) if (option) { const errors: TriggerFormDataErrors = {}; errors.event = validateEventKind(selectedType, option.key as string); - setFormData({ ...formData, $kind: option.key as string, errors: { ...formData.errors, ...errors } }); + setFormData({ + ...formData, + $kind: option.key as string, + errors: { ...formData.errors, ...errors }, + }); } }; @@ -381,13 +362,21 @@ export const TriggerCreationModal: React.FC = (props) if (showTriggerPhrase && formData.triggerPhrases) { errors.triggerPhrases = getLuDiagnostics(name, formData.triggerPhrases); } - setFormData({ ...formData, intent: name, errors: { ...formData.errors, ...errors } }); + setFormData({ + ...formData, + intent: name, + errors: { ...formData.errors, ...errors }, + }); }; const onChangeRegEx = (e, pattern) => { const errors: TriggerFormDataErrors = {}; errors.regEx = validateRegExPattern(selectedType, isRegEx, pattern); - setFormData({ ...formData, regEx: pattern, errors: { ...formData.errors, ...errors } }); + setFormData({ + ...formData, + regEx: pattern, + errors: { ...formData.errors, ...errors }, + }); }; //Trigger phrase is optional @@ -398,7 +387,11 @@ export const TriggerCreationModal: React.FC = (props) } else { errors.triggerPhrases = ''; } - setFormData({ ...formData, triggerPhrases: body, errors: { ...formData.errors, ...errors } }); + setFormData({ + ...formData, + triggerPhrases: body, + errors: { ...formData.errors, ...errors }, + }); }; const errors = validateForm(selectedType, formData, isRegEx, regexIntents); const disable = shouldDisable(errors); diff --git a/Composer/packages/client/src/hooks/useElectronFeatures.ts b/Composer/packages/client/src/hooks/useElectronFeatures.ts new file mode 100644 index 0000000000..3f5846eba2 --- /dev/null +++ b/Composer/packages/client/src/hooks/useElectronFeatures.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useEffect } from 'react'; +import get from 'lodash/get'; +import { getEditorAPI } from '@bfc/shared'; + +export const useElectronFeatures = (actionSelected: boolean) => { + // Sync selection state to Electron main process + useEffect(() => { + if (!window.__IS_ELECTRON__) return; + if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') return; + + window.ipcRenderer.send('composer-state-change', { actionSelected }); + }, [actionSelected]); + + // Subscribe Electron app menu events (copy/cut/del/undo/redo) + useEffect(() => { + if (!window.__IS_ELECTRON__) return; + if (!window.ipcRenderer || typeof window.ipcRenderer.on !== 'function') return; + + const EditorAPI = getEditorAPI(); + window.ipcRenderer.on('electron-menu-clicked', (e, data) => { + const label = get(data, 'label', ''); + switch (label) { + case 'undo': + return EditorAPI.Editing.Undo(); + case 'redo': + return EditorAPI.Editing.Redo(); + case 'cut': + return EditorAPI.Actions.CutSelection(); + case 'copy': + return EditorAPI.Actions.CopySelection(); + case 'delete': + return EditorAPI.Actions.DeleteSelection(); + } + }); + }, []); +}; diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index c5df32b3d2..b0f43fc736 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -10,7 +10,7 @@ import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcru import formatMessage from 'format-message'; import { globalHistory, RouteComponentProps } from '@reach/router'; import get from 'lodash/get'; -import { DialogFactory, SDKKinds, DialogInfo, PromptTab } from '@bfc/shared'; +import { DialogFactory, SDKKinds, DialogInfo, PromptTab, LuIntentSection, getEditorAPI, LgTemplate } from '@bfc/shared'; import { ActionButton, Button } from 'office-ui-fabric-react/lib/Button'; import { JsonEditor } from '@bfc/code-editor'; import { useTriggerApi } from '@bfc/extension'; @@ -20,7 +20,6 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'; import { TestController } from '../../components/TestController/TestController'; import { DialogDeleting } from '../../constants'; import { createSelectedPath, deleteTrigger, getbreadcrumbLabel } from '../../utils/dialogUtil'; -import { LuFilePayload, LgFilePayload } from '../../components/ProjectTree/TriggerCreationModal'; import { Conversation } from '../../components/Conversation'; import { dialogStyle } from '../../components/Modal/dialogStyle'; import { OpenConfirmModal } from '../../components/Modal/ConfirmDialog'; @@ -46,11 +45,14 @@ import { skillsState, actionsSeedState, userSettingsState, + luFilesState, + localeState, qnaFilesState, + lgFilesState, } from '../../recoilModel'; import { getBaseName } from '../../utils/fileUtil'; +import { useElectronFeatures } from '../../hooks/useElectronFeatures'; -import { VisualEditorAPI } from './FrameAPI'; import { breadcrumbClass, contentWrapper, @@ -147,6 +149,10 @@ const DesignPage: React.FC id === `${dialogId}.${locale}`); + const lgFile = lgFiles.find(({ id }) => id === `${dialogId}.${locale}`); const params = new URLSearchParams(location?.search); const selected = params.get('selected') || ''; const [triggerModalVisible, setTriggerModalVisibility] = useState(false); @@ -270,28 +278,24 @@ const DesignPage: React.FC { + const onTriggerCreationSubmit = async (dialog: DialogInfo, intent?: LuIntentSection, lgTemplates?: LgTemplate[]) => { const dialogPayload = { id: dialog.id, projectId, content: dialog.content, }; - if (luFile) { - const luFilePayload = { - id: luFile.id, - content: luFile.content, - projectId, - }; - updateLuFile(luFilePayload); + if (luFile && intent) { + createLuIntent({ id: luFile.id, intent }); } - if (lgFile) { - const lgFilePayload = { - id: lgFile.id, - content: lgFile.content, - projectId, - }; - await updateLgFile(lgFilePayload); + if (lgFile && lgTemplates) { + lgTemplates.forEach(async (t) => { + const lgPayload = { + id: lgFile.id, + template: t as LgTemplate, + }; + await createLgTemplate(lgPayload); + }); } const index = get(dialog, 'content.triggers', []).length - 1; @@ -316,7 +320,7 @@ const DesignPage: React.FC { const actionSelected = Array.isArray(visualEditorSelection) && visualEditorSelection.length > 0; if (!actionSelected) { - return {}; + return { actionSelected: false, showDisableBtn: false, showEnableBtn: false }; } const selectedActions = visualEditorSelection.map((id) => get(currentDialog?.content, id)); const showDisableBtn = selectedActions.some((x) => get(x, 'disabled') !== true); @@ -324,6 +328,9 @@ const DesignPage: React.FC { + // TODO: register EditorAPI.Editing.Undo() //ToDo undo }, }, @@ -379,6 +387,7 @@ const DesignPage: React.FC { + // TODO: register EditorAPI.Editing.Redo() //ToDo redo }, }, @@ -387,7 +396,7 @@ const DesignPage: React.FC { - VisualEditorAPI.cutSelection(); + EditorAPI.Actions.CutSelection(); }, }, { @@ -395,7 +404,7 @@ const DesignPage: React.FC { - VisualEditorAPI.copySelection(); + EditorAPI.Actions.CopySelection(); }, }, { @@ -403,7 +412,7 @@ const DesignPage: React.FC { - VisualEditorAPI.moveSelection(); + EditorAPI.Actions.MoveSelection(); }, }, { @@ -411,7 +420,7 @@ const DesignPage: React.FC { - VisualEditorAPI.deleteSelection(); + EditorAPI.Actions.DeleteSelection(); }, }, ], @@ -432,7 +441,7 @@ const DesignPage: React.FC { - VisualEditorAPI.disableSelection(); + EditorAPI.Actions.DisableSelection(); }, }, { @@ -440,7 +449,7 @@ const DesignPage: React.FC { - VisualEditorAPI.enableSelection(); + EditorAPI.Actions.EnableSelection(); }, }, ], diff --git a/Composer/packages/client/src/pages/design/FrameAPI.ts b/Composer/packages/client/src/pages/design/FrameAPI.ts deleted file mode 100644 index a26e168a3e..0000000000 --- a/Composer/packages/client/src/pages/design/FrameAPI.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export class FrameAPI { - /** - * Initialize the frame ref at first invocation. - */ - invoke = (method: string, ...rest: any[]) => { - if (typeof window[method] === 'function') { - return window[method](...rest); - } - }; -} - -export const VisualEditorAPI = (() => { - const visualEditorFrameAPI = new FrameAPI(); - // HACK: under cypress env, avoid invoking API inside frame too frequently (especially the `hasEleemntFocused`). It will lead to CI test quite fagile. - // TODO: remove this hack logic after refactoring state sync logic between shell and editors. - if ((window as any).Cypress) { - visualEditorFrameAPI.invoke = () => {}; - } - - return { - copySelection: () => visualEditorFrameAPI.invoke('copySelection'), - cutSelection: () => visualEditorFrameAPI.invoke('cutSelection'), - moveSelection: () => visualEditorFrameAPI.invoke('moveSelection'), - deleteSelection: () => visualEditorFrameAPI.invoke('deleteSelection'), - disableSelection: () => visualEditorFrameAPI.invoke('disableSelection'), - enableSelection: () => visualEditorFrameAPI.invoke('enableSelection'), - }; -})(); diff --git a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx index defcea5505..27caca3530 100644 --- a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx +++ b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx @@ -25,7 +25,7 @@ import { import { languageListTemplates } from '../../../components/MultiLanguage'; import { navigateTo } from '../../../utils/navigation'; -import { hostedControls, settingsEditor, toolbar } from './style'; +import { settingsEditor, toolbar } from './style'; import { BotSettings } from './constants'; export const DialogSettings: React.FC = () => { @@ -96,22 +96,20 @@ export const DialogSettings: React.FC = () => { const editorId = [defaultLanguage, ...languages].join('-'); return botName ? ( - + -
- -

- {BotSettings.botSettingDescription} -   - - {BotSettings.learnMore} - -

-
+ +
+ {BotSettings.botSettingDescription} +   + + {BotSettings.learnMore} + +
-

{BotSettings.languagesubTitle}

+
{BotSettings.languagesubTitle}
h1 { - margin-top: 0; - } -`; - export const settingsEditor = css` flex: 1; height: 500px; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx index ca53a4d3a0..38f1927429 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx @@ -5,6 +5,7 @@ import { useRecoilState } from 'recoil'; import { LuIntentSection, LuFile } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import { act } from '@bfc/test-utils/lib/hooks'; +import { luUtil } from '@bfc/indexers'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; import { luFilesState } from '../../atoms'; @@ -22,12 +23,13 @@ jest.mock('../../parsers/luWorker', () => { removeIntents: require('@bfc/indexers/lib/utils/luUtil').removeIntents, }; }); -const luFiles = [ - { - id: 'common.en-us', - content: `\r\n# Hello\r\n-hi`, - }, -] as LuFile[]; + +const file1 = { + id: 'common.en-us', + content: `\r\n# Hello\r\n-hi`, +}; + +const luFiles = [luUtil.parse(file1.id, file1.content)] as LuFile[]; const getLuIntent = (Name, Body): LuIntentSection => ({ @@ -96,7 +98,7 @@ describe('Lu dispatcher', () => { expect(renderedComponent.current.luFiles[0].content).toMatch(/-IntentValue/); }); - it('should remove a lg template', async () => { + it('should remove a lu intent', async () => { await act(async () => { await dispatcher.removeLuIntent({ id: luFiles[0].id, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index 9e031e21b7..9c24d56c08 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -4,6 +4,7 @@ import { LgTemplate, LgFile, importResolverGenerator } from '@bfc/shared'; import { useRecoilCallback, CallbackInterface } from 'recoil'; import differenceBy from 'lodash/differenceBy'; +import formatMessage from 'format-message'; import { getBaseName, getExtension } from '../../utils/fileUtil'; @@ -20,13 +21,13 @@ const initialBody = '- '; export const updateLgFileState = async ( callbackHelpers: CallbackInterface, - { id, content }: { id: string; content: string } + { id, content, updatedFile }: { id: string; content: string; updatedFile?: LgFile } ) => { const { set, snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); const dialogId = getBaseName(id); const locale = getExtension(id); - const updatedLgFile = (await LgWorker.parse(id, content, lgFiles)) as LgFile; + const updatedLgFile = updatedFile || ((await LgWorker.parse(id, content, lgFiles)) as LgFile); const originLgFile = lgFiles.find((file) => id === file.id); const sameIdOtherLocaleFiles = lgFiles.filter((file) => { const fileDialogId = getBaseName(file.id); @@ -35,7 +36,7 @@ export const updateLgFileState = async ( }); if (!originLgFile) { - throw new Error('origin lg file not found in store'); + throw new Error(formatMessage('origin lg file not found in store')); } const changes: LgFile[] = [updatedLgFile]; @@ -58,10 +59,9 @@ export const updateLgFileState = async ( if (onlyAdds || onlyDeletes) { for (const file of sameIdOtherLocaleFiles) { const lgImportResolver = importResolverGenerator(lgFiles, '.lg', getExtension(file.id)); - let newLgFile = lgUtil.addTemplates(file.id, file.content, addedTemplates, lgImportResolver); + let newLgFile = lgUtil.addTemplates(file, addedTemplates, lgImportResolver); newLgFile = lgUtil.removeTemplates( - file.id, - newLgFile.content, + newLgFile, deletedTemplates.map(({ name }) => name), lgImportResolver ); @@ -92,7 +92,7 @@ export const createLgFileState = async ( const { languages } = await snapshot.getPromise(settingsState); const createdLgId = `${id}.${locale}`; if (lgFiles.find((lg) => lg.id === createdLgId)) { - throw new Error('lg file already exist'); + throw new Error(formatMessage('lg file already exist')); } // slot with common.lg import let lgInitialContent = ''; @@ -101,7 +101,7 @@ export const createLgFileState = async ( lgInitialContent = `[import](common.lg)`; } content = [lgInitialContent, content].join('\n'); - const createdLgFile = (await LgWorker.parse(createdLgId, content, lgFiles)) as LgFile; + const createdLgFile = lgUtil.parse(createdLgId, content, lgFiles); const changes: LgFile[] = []; // copy to other locales @@ -155,9 +155,12 @@ export const lgDispatcher = () => { }) => { const { snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); - let content = lgFiles.find((file) => file.id === id)?.content ?? ''; - content = lgUtil.updateTemplate(id, content, templateName, template, lgFileResolver(lgFiles)).content; - await updateLgFileState(callbackHelpers, { id, content }); + const lgFile = lgFiles.find((file) => file.id === id); + if (!lgFile) { + throw new Error(formatMessage('lg file {id} does not exist.', { id })); + } + const updatedFile = lgUtil.updateTemplate(lgFile, templateName, template, lgFileResolver(lgFiles)); + await updateLgFileState(callbackHelpers, { id, content: updatedFile.content, updatedFile }); } ); @@ -165,9 +168,13 @@ export const lgDispatcher = () => { (callbackHelpers: CallbackInterface) => async ({ id, template }: { id: string; template: LgTemplate }) => { const { snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); - let content = lgFiles.find((file) => file.id === id)?.content ?? ''; - content = lgUtil.addTemplate(id, content, template, lgFileResolver(lgFiles)).content; - await updateLgFileState(callbackHelpers, { id, content }); + const lgFile = lgFiles.find((file) => file.id === id); + if (!lgFile) { + throw new Error(formatMessage('lg file {id} does not exist.', { id })); + } + + const updatedFile = lgUtil.addTemplate(lgFile, template, lgFileResolver(lgFiles)); + await updateLgFileState(callbackHelpers, { id, updatedFile, content: updatedFile.content }); } ); @@ -175,9 +182,13 @@ export const lgDispatcher = () => { (callbackHelpers: CallbackInterface) => async ({ id, templateName }: { id: string; templateName: string }) => { const { snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); - let content = lgFiles.find((file) => file.id === id)?.content ?? ''; - content = lgUtil.removeTemplate(id, content, templateName, lgFileResolver(lgFiles)).content; - await updateLgFileState(callbackHelpers, { id, content }); + const lgFile = lgFiles.find((file) => file.id === id); + if (!lgFile) { + throw new Error(formatMessage('lg file {id} does not exist.', { id })); + } + + const updatedFile = lgUtil.removeTemplate(lgFile, templateName, lgFileResolver(lgFiles)); + await updateLgFileState(callbackHelpers, { id, updatedFile, content: updatedFile.content }); } ); @@ -185,9 +196,12 @@ export const lgDispatcher = () => { (callbackHelpers: CallbackInterface) => async ({ id, templateNames }: { id: string; templateNames: string[] }) => { const { snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); - let content = lgFiles.find((file) => file.id === id)?.content ?? ''; - content = lgUtil.removeTemplates(id, content, templateNames, lgFileResolver(lgFiles)).content; - await updateLgFileState(callbackHelpers, { id, content }); + const lgFile = lgFiles.find((file) => file.id === id); + if (!lgFile) { + throw new Error(formatMessage('lg file {id} does not exist.', { id })); + } + const updatedFile = lgUtil.removeTemplates(lgFile, templateNames, lgFileResolver(lgFiles)); + await updateLgFileState(callbackHelpers, { id, updatedFile, content: updatedFile.content }); } ); @@ -203,9 +217,12 @@ export const lgDispatcher = () => { }) => { const { snapshot } = callbackHelpers; const lgFiles = await snapshot.getPromise(lgFilesState); - let content = lgFiles.find((file) => file.id === id)?.content ?? ''; - content = lgUtil.copyTemplate(id, content, fromTemplateName, toTemplateName, lgFileResolver(lgFiles)).content; - await updateLgFileState(callbackHelpers, { id, content }); + const lgFile = lgFiles.find((file) => file.id === id); + if (!lgFile) { + throw new Error(formatMessage('lg file {id} does not exist.', { id })); + } + const updatedFile = lgUtil.copyTemplate(lgFile, fromTemplateName, toTemplateName, lgFileResolver(lgFiles)); + await updateLgFileState(callbackHelpers, { id, updatedFile, content: updatedFile.content }); } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts index 97fd9f1846..d21c1e4ccf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts @@ -4,10 +4,12 @@ import { LuFile, LuIntentSection } from '@bfc/shared'; import { useRecoilCallback, CallbackInterface } from 'recoil'; import differenceBy from 'lodash/differenceBy'; +import formatMessage from 'format-message'; +import luWorker from '../parsers/luWorker'; import { getBaseName, getExtension } from '../../utils/fileUtil'; +import * as luUtil from '../../utils/luUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; -import luWorker from '../parsers/luWorker'; import { luFilesState, projectIdState, localeState, settingsState } from '../atoms/botState'; const intentIsNotEmpty = ({ Name, Body }) => { @@ -18,7 +20,7 @@ const initialBody = '- '; export const updateLuFileState = async ( callbackHelpers: CallbackInterface, - { id, content }: { id: string; content: string } + { id, content, updatedFile }: { id: string; content: string; updatedFile?: LuFile } ) => { const { set, snapshot } = callbackHelpers; const luFiles = await snapshot.getPromise(luFilesState); @@ -26,7 +28,7 @@ export const updateLuFileState = async ( const dialogId = getBaseName(id); const locale = getExtension(id); - const updatedLuFile = (await luWorker.parse(id, content)) as LuFile; + const updatedLuFile = updatedFile || ((await luWorker.parse(id, content)) as LuFile); // if exist updated luFile, do not parse again. const originLuFile = luFiles.find((file) => id === file.id); const sameIdOtherLocaleFiles = luFiles.filter((file) => { const fileDialogId = getBaseName(file.id); @@ -35,7 +37,7 @@ export const updateLuFileState = async ( }); if (!originLuFile) { - throw new Error('origin lu file not found in store'); + throw new Error(formatMessage('origin lu file not found in store')); } const changes: LuFile[] = [updatedLuFile]; @@ -53,13 +55,12 @@ export const updateLuFileState = async ( const onlyDeletes = !addedIntents.length && deletedIntents.length; // sync add/remove intents if (onlyAdds || onlyDeletes) { - for (const { id, content } of sameIdOtherLocaleFiles) { - let newContent: string = (await luWorker.addIntents(content, addedIntents)) as string; - newContent = (await luWorker.removeIntents( - newContent, + for (const item of sameIdOtherLocaleFiles) { + let newLuFile = luUtil.addIntents(item, addedIntents); + newLuFile = luUtil.removeIntents( + newLuFile, deletedIntents.map(({ Name }) => Name) - )) as string; - const newLuFile = (await luWorker.parse(id, newContent)) as LuFile; + ); changes.push(newLuFile); } } @@ -89,7 +90,7 @@ export const createLuFileState = async ( const locale = await snapshot.getPromise(localeState); const { languages } = await snapshot.getPromise(settingsState); const createdLuId = `${id}.${locale}`; - const createdLuFile = (await luWorker.parse(id, content)) as LuFile; + const createdLuFile = (await luUtil.parse(id, content)) as LuFile; if (luFiles.find((lu) => lu.id === createdLuId)) { throw new Error('lu file already exist'); } @@ -150,8 +151,8 @@ export const luDispatcher = () => { const luFiles = await callbackHelpers.snapshot.getPromise(luFilesState); const file = luFiles.find((temp) => temp.id === id); if (!file) return; - const content = (await luWorker.updateIntent(file.content, intentName, intent)) as string; - await updateLuFileState(callbackHelpers, { id: file.id, content }); + const updatedFile = luUtil.updateIntent(file, intentName, intent); + await updateLuFileState(callbackHelpers, { id: file.id, updatedFile, content: updatedFile.content }); } ); @@ -160,8 +161,8 @@ export const luDispatcher = () => { const luFiles = await callbackHelpers.snapshot.getPromise(luFilesState); const file = luFiles.find((temp) => temp.id === id); if (!file) return; - const content = (await luWorker.addIntent(file.content, intent)) as string; - await updateLuFileState(callbackHelpers, { id: file.id, content }); + const updatedFile = luUtil.addIntent(file, intent); + await updateLuFileState(callbackHelpers, { id: file.id, updatedFile, content: updatedFile.content }); } ); @@ -170,8 +171,8 @@ export const luDispatcher = () => { const luFiles = await callbackHelpers.snapshot.getPromise(luFilesState); const file = luFiles.find((temp) => temp.id === id); if (!file) return; - const content = (await luWorker.removeIntent(file.content, intentName)) as string; - await updateLuFileState(callbackHelpers, { id: file.id, content }); + const updatedFile = luUtil.removeIntent(file, intentName); + await updateLuFileState(callbackHelpers, { id: file.id, updatedFile, content: updatedFile.content }); } ); diff --git a/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts b/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts index c540913e01..2d980dcda8 100644 --- a/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts +++ b/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts @@ -28,7 +28,7 @@ const lgFiles = [ describe('test lg worker', () => { it('get expected parse result', async () => { const result: any = await lgWorker.parse('common.en-us', `\r\n# Hello\r\n-hi`, lgFiles); - const expected = [{ body: '-hi', name: 'Hello', parameters: [], range: { endLineNumber: 0, startLineNumber: 0 } }]; + const expected = [{ body: '-hi', name: 'Hello', parameters: [], range: { endLineNumber: 3, startLineNumber: 2 } }]; expect(result.templates).toMatchObject(expected); }); }); diff --git a/Composer/packages/client/src/recoilModel/parsers/__test__/luWorker.test.ts b/Composer/packages/client/src/recoilModel/parsers/__test__/luWorker.test.ts index 70dba60d7d..abe759eda7 100644 --- a/Composer/packages/client/src/recoilModel/parsers/__test__/luWorker.test.ts +++ b/Composer/packages/client/src/recoilModel/parsers/__test__/luWorker.test.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LuIntentSection } from '@bfc/shared'; - import luWorker from '../luWorker'; jest.mock('./../workers/luParser.worker.ts', () => { @@ -18,12 +16,6 @@ jest.mock('./../workers/luParser.worker.ts', () => { return Test; }); -const getLuIntent = (Name, Body): LuIntentSection => - ({ - Name, - Body, - } as LuIntentSection); - describe('test lu worker', () => { it('get expected parse result', async () => { const content = `# Hello @@ -49,41 +41,4 @@ hi expect(diagnostics[0].range.end.line).toEqual(2); expect(diagnostics[0].range.end.character).toEqual(2); }); - - it('should add an intent', async () => { - const content = `# Greeting - hi - - hello - - @ simple friendsName - - `; - const result: any = await luWorker.addIntent(content, getLuIntent('Hello', '-IntentValue')); - expect(result).toContain('-IntentValue'); - }); - - it('should remove an intent', async () => { - const content = `# Greeting - hi - - hello - - @ simple friendsName - - `; - const result: any = await luWorker.removeIntent(content, 'Greeting'); - expect(result).not.toContain('- hello'); - }); - - it('should update an intent', async () => { - const content = `# Greeting - hi - - hello - - @ simple friendsName - - `; - const result: any = await luWorker.updateIntent(content, 'Greeting', getLuIntent('Greeting', '-IntentValue')); - expect(result).not.toContain('- hello'); - expect(result).toContain('-IntentValue'); - }); }); diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts index cbcb1905f9..e5be382440 100644 --- a/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts +++ b/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts @@ -76,37 +76,6 @@ export const handleMessage = (msg: LgMessageEvent) => { payload = { id, content, templates, diagnostics }; break; } - case LgActionType.AddTemplate: { - const { id, content, template } = msg.payload; - payload = lgUtil.addTemplate(id, content, template); - break; - } - case LgActionType.AddTemplates: { - const { id, content, templates } = msg.payload; - payload = lgUtil.addTemplates(id, content, templates); - break; - } - case LgActionType.UpdateTemplate: { - const { id, content, templateName, template } = msg.payload; - lgUtil.checkSingleLgTemplate(template); - payload = lgUtil.updateTemplate(id, content, templateName, template); - break; - } - case LgActionType.RemoveTemplate: { - const { id, content, templateName } = msg.payload; - payload = lgUtil.removeTemplate(id, content, templateName); - break; - } - case LgActionType.RemoveAllTemplates: { - const { id, content, templateNames } = msg.payload; - payload = lgUtil.removeTemplates(id, content, templateNames); - break; - } - case LgActionType.CopyTemplate: { - const { id, content, toTemplateName, fromTemplateName } = msg.payload; - payload = lgUtil.copyTemplate(id, content, fromTemplateName, toTemplateName); - break; - } } return payload; }; diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts index cc9d7b3d7d..fd87c3c569 100644 --- a/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts +++ b/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts @@ -8,33 +8,13 @@ const ctx: Worker = self as any; export const handleMessage = (msg) => { const { type, payload } = msg.data; - const { content, id, intentName, intentNames, intent, intents } = payload; + const { content, id } = payload; let result: any = null; switch (type) { case LuActionType.Parse: { result = luUtil.parse(id, content); break; } - case LuActionType.AddIntent: { - result = luUtil.addIntent(content, intent); - break; - } - case LuActionType.AddIntents: { - result = luUtil.addIntents(content, intents); - break; - } - case LuActionType.UpdateIntent: { - result = luUtil.updateIntent(content, intentName, intent || null); - break; - } - case LuActionType.RemoveIntent: { - result = luUtil.removeIntent(content, intentName); - break; - } - case LuActionType.RemoveIntents: { - result = luUtil.removeIntents(content, intentNames); - break; - } } return result; }; diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index d433cbad00..50c116dee7 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -72,7 +72,11 @@ function updateTriggerActionsAttributes(trigger, extraTriggerAttributes) { t.actions[0].activity = `$\{SendActivity_${lgTemplateId}()}`; } if (t.$kind === SDKKinds.OnChooseIntent) { - t.actions[1].prompt = `$\{ChoiceInput_Prompt_${lgTemplateId}}`; + //t.actions[1].prompt = `$\{ChoiceInput_Prompt_${lgTemplateId}}`; + for (const key in extraTriggerAttributes) { + set(t, key, extraTriggerAttributes[key]); + console.log(get(t, key, '')); + } } return t; } diff --git a/Composer/packages/electron-server/__tests__/appMenu.test.ts b/Composer/packages/electron-server/__tests__/appMenu.test.ts index 7d61261100..3227f04f96 100644 --- a/Composer/packages/electron-server/__tests__/appMenu.test.ts +++ b/Composer/packages/electron-server/__tests__/appMenu.test.ts @@ -40,7 +40,7 @@ describe('App menu', () => { // Edit expect(menuTemplate[1].label).toBe('Edit'); - expect(menuTemplate[1].submenu.length).toBe(9); + expect(menuTemplate[1].submenu.length).toBe(8); // View expect(menuTemplate[2].label).toBe('View'); @@ -75,7 +75,7 @@ describe('App menu', () => { // Edit expect(menuTemplate[2].label).toBe('Edit'); - expect(menuTemplate[2].submenu.length).toBe(9); + expect(menuTemplate[2].submenu.length).toBe(8); // View expect(menuTemplate[3].label).toBe('View'); diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index e61e6d7a70..f6e877c441 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { app, dialog, Menu, MenuItemConstructorOptions, shell } from 'electron'; +import { app, dialog, Menu, MenuItemConstructorOptions, shell, ipcMain } from 'electron'; import { isMac } from './utility/platform'; import { AppUpdater } from './appUpdater'; @@ -29,7 +29,6 @@ function getAppMenu(): MenuItemConstructorOptions[] { function getRestOfEditMenu(): MenuItemConstructorOptions[] { if (isMac()) { return [ - { role: 'delete' }, { type: 'separator' }, { label: 'Speech', @@ -37,7 +36,7 @@ function getRestOfEditMenu(): MenuItemConstructorOptions[] { }, ]; } - return [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]; + return [{ type: 'separator' }, { role: 'selectAll' }]; } function getRestOfWindowMenu(): MenuItemConstructorOptions[] { @@ -47,7 +46,14 @@ function getRestOfWindowMenu(): MenuItemConstructorOptions[] { return [{ role: 'close' }]; } -export function initAppMenu() { +export function initAppMenu(win?: Electron.BrowserWindow) { + // delegate menu events to Renderer process (Composer web app) + const handleMenuEvents = (menuEventName: string) => { + if (win) { + win.webContents.send('electron-menu-clicked', { label: menuEventName }); + } + }; + const template: MenuItemConstructorOptions[] = [ // App (Mac) ...getAppMenu(), @@ -60,12 +66,25 @@ export function initAppMenu() { { label: 'Edit', submenu: [ - { role: 'undo' }, - { role: 'redo' }, + // NOTE: Avoid using builtin `role`, it won't override the click handler. + { id: 'Undo', label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') }, + { id: 'Redo', label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => handleMenuEvents('redo') }, { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, + { id: 'Cut', label: 'Cut', enabled: false, accelerator: 'CmdOrCtrl+X', click: () => handleMenuEvents('cut') }, + { + id: 'Copy', + label: 'Copy', + enabled: false, + accelerator: 'CmdOrCtrl+C', + click: () => handleMenuEvents('copy'), + }, + { + id: 'Delete', + label: 'Delete', + enabled: false, + accelerator: 'Delete', + click: () => handleMenuEvents('delete'), + }, ...getRestOfEditMenu(), ], }, @@ -161,4 +180,15 @@ export function initAppMenu() { const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); + + // Let menu enable/disable status reflect action selection states. + ipcMain && + ipcMain.on && + ipcMain.on('composer-state-change', (e, state) => { + const actionSelected = !!state.actionSelected; + ['Cut', 'Copy', 'Delete'].forEach((id) => { + menu.getMenuItemById(id).enabled = actionSelected; + }); + Menu.setApplicationMenu(menu); + }); } diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 79374bb772..4082227967 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -131,8 +131,8 @@ async function loadServer() { async function main() { log('Rendering application...'); - initAppMenu(); const mainWindow = ElectronWindow.getInstance().browserWindow; + initAppMenu(mainWindow); if (mainWindow) { if (process.env.COMPOSER_DEV_TOOLS) { mainWindow.webContents.openDevTools(); diff --git a/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts b/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts index bbfa51d423..8739daa91a 100644 --- a/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts +++ b/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DialogUtils, SDKKinds, ShellApi } from '@bfc/shared'; +import { DialogUtils, SDKKinds, ShellApi, registerEditorAPI } from '@bfc/shared'; import get from 'lodash/get'; import { useDialogEditApi, useDialogApi, useActionApi } from '@bfc/extension'; @@ -311,13 +311,14 @@ export const useEditorEventApi = ( return handler(eventData); }; - // HACK: use global handler before we solve iframe state sync problem - (window as any).copySelection = () => handleEditorEvent(NodeEventTypes.CopySelection); - (window as any).cutSelection = () => handleEditorEvent(NodeEventTypes.CutSelection); - (window as any).moveSelection = () => handleEditorEvent(NodeEventTypes.MoveSelection); - (window as any).deleteSelection = () => handleEditorEvent(NodeEventTypes.DeleteSelection); - (window as any).disableSelection = () => handleEditorEvent(NodeEventTypes.DisableSelection); - (window as any).enableSelection = () => handleEditorEvent(NodeEventTypes.EnableSelection); + registerEditorAPI('Actions', { + CopySelection: () => handleEditorEvent(NodeEventTypes.CopySelection), + CutSelection: () => handleEditorEvent(NodeEventTypes.CutSelection), + MoveSelection: () => handleEditorEvent(NodeEventTypes.MoveSelection), + DeleteSelection: () => handleEditorEvent(NodeEventTypes.DeleteSelection), + DisableSelection: () => handleEditorEvent(NodeEventTypes.DisableSelection), + EnableSelection: () => handleEditorEvent(NodeEventTypes.EnableSelection), + }); return { handleEditorEvent, diff --git a/Composer/packages/lib/indexers/__tests__/lgUtil.test.ts b/Composer/packages/lib/indexers/__tests__/lgUtil.test.ts index 662b31dae3..36f278ea31 100644 --- a/Composer/packages/lib/indexers/__tests__/lgUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/lgUtil.test.ts @@ -3,9 +3,41 @@ import { Templates } from 'botbuilder-lg'; -import { updateTemplate, addTemplate, checkTemplate, removeTemplate, extractOptionByKey } from '../src/utils/lgUtil'; +import { + updateTemplate, + addTemplate, + checkTemplate, + removeTemplate, + extractOptionByKey, + parse, +} from '../src/utils/lgUtil'; describe('update lg template', () => { + it('should parse lg file', () => { + const content = `# Exit +-Thanks for using todo bot. + +# Greeting +-What's up bro`; + + const templates = parse('a.lg', content, []).templates; + expect(templates.length).toEqual(2); + expect(templates[0].name).toEqual('Exit'); + expect(templates[0].body).toContain('-Thanks for using todo bot.'); + expect(templates[0].parameters).toEqual([]); + expect(templates[0].range).toEqual({ + startLineNumber: 1, + endLineNumber: 3, + }); + expect(templates[1].name).toEqual('Greeting'); + expect(templates[1].body).toContain(`-What's up bro`); + expect(templates[1].parameters).toEqual([]); + expect(templates[1].range).toEqual({ + startLineNumber: 4, + endLineNumber: 5, + }); + }); + it('should update lg template', () => { const content = `# Exit -Thanks for using todo bot. @@ -13,9 +45,10 @@ describe('update lg template', () => { # Greeting -What's up bro`; + const lgFile = parse('a.lg', content, []); const template = { name: 'Exit', parameters: [], body: '-Bye' }; - const newContent = updateTemplate(content, 'Exit', template); - const templates = Templates.parseText(newContent).toArray(); + const updatedLgFile = updateTemplate(lgFile, 'Exit', template); + const templates = Templates.parseText(updatedLgFile.content).toArray(); expect(templates.length).toEqual(2); expect(templates[0].name).toEqual('Exit'); expect(templates[0].body).toEqual('-Bye'); @@ -27,12 +60,13 @@ describe('update lg template', () => { # Greeting -What's up bro`; + const lgFile = parse('a.lg', content, []); const templates0 = Templates.parseText(content).toArray(); expect(templates0.length).toEqual(2); const template = { name: 'Exit', parameters: [], body: '-Bye' }; - const newContent = updateTemplate(content, 'Exit', template); - const templates = Templates.parseText(newContent).toArray(); + const updatedLgFile = updateTemplate(lgFile, 'Exit', template); + const templates = Templates.parseText(updatedLgFile.content).toArray(); expect(templates.length).toEqual(2); expect(templates[0].name).toEqual('Exit'); expect(templates[0].body).toEqual('-Bye'); @@ -46,9 +80,10 @@ describe('add lg template', () => { # Greeting -What's up bro`; + const lgFile = parse('a.lg', content, []); const template = { name: 'Hi', parameters: [], body: '-hello' }; - const newContent = addTemplate(content, template); - const templates = Templates.parseText(newContent).toArray(); + const updatedLgFile = addTemplate(lgFile, template); + const templates = Templates.parseText(updatedLgFile.content).toArray(); expect(templates.length).toEqual(3); expect(templates[0].name).toEqual('Exit'); expect(templates[1].name).toEqual('Greeting'); @@ -63,8 +98,9 @@ describe('remove lg template', () => { # Greeting -What's up bro`; - const newContent = removeTemplate(content, 'Greeting'); - const templates = Templates.parseText(newContent).toArray(); + const lgFile = parse('a.lg', content, []); + const updatedLgFile = removeTemplate(lgFile, 'Greeting'); + const templates = Templates.parseText(updatedLgFile.content).toArray(); expect(templates.length).toEqual(1); expect(templates[0].name).toEqual('Exit'); }); diff --git a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts index 5f79d900ea..4068b2267c 100644 --- a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import { updateIntent, addIntent, removeIntent } from '../src/utils/luUtil'; +import { luIndexer } from '../src/luIndexer'; const { luParser, luSectionTypes } = sectionHandler; @@ -29,9 +29,11 @@ hi # Foo > nothing in body `; + const fileId1 = 'a.lu'; + const fileId2 = 'b.lu'; it('parse section test', () => { - const luresource = luParser.parse(fileContent); + const luresource = luIndexer.parse(fileContent, fileId1).resource; const { Sections, Errors, Content } = luresource; expect(Content).toEqual(fileContent); @@ -46,7 +48,7 @@ hi }); it('parse section with syntax error test', () => { - const luresource = luParser.parse(fileContentError1); + const luresource = luIndexer.parse(fileContentError1, fileId2).resource; const { Sections, Errors, Content } = luresource; expect(Content).toEqual(fileContentError1); @@ -62,9 +64,10 @@ hi Body: `- check my unread email - show my unread emails`, }; + const luFile1 = luIndexer.parse(fileContent, fileId1); - const fileContentUpdated = addIntent(fileContent, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1Updated = addIntent(luFile1, intent); + const luresource = luParser.parse(luFile1Updated.content); const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -87,8 +90,15 @@ hi - check my mail box please`, }; - const fileContentUpdated = updateIntent(fileContent, intentName, intent); - const luresource = luParser.parse(fileContentUpdated); + const intent2 = { + Name: 'CheckEmail', + Body: `- check my email +- show my emails 2 +- check my mail box please`, + }; + const luFile1 = luIndexer.parse(fileContent, fileId1); + const updatedLuFile = updateIntent(luFile1, intentName, intent); + const luresource = updatedLuFile.resource; const { Sections, Errors } = luresource; @@ -101,6 +111,21 @@ hi expect(luresource.Sections[1].UtteranceAndEntitiesMap[0].utterance).toEqual('check my email'); expect(luresource.Sections[1].UtteranceAndEntitiesMap[1].utterance).toEqual('show my emails'); expect(luresource.Sections[1].UtteranceAndEntitiesMap[2].utterance).toEqual('check my mail box please'); + + // continue update on luresource + const updatedLuFile2 = updateIntent(updatedLuFile, intentName, intent2); + const luresource2 = updatedLuFile2.resource; + + expect(luresource2.Errors.length).toEqual(0); + expect(luresource2.Sections.length).toEqual(2); + expect(luresource2.Sections[1].Errors.length).toEqual(0); + expect(luresource2.Sections[1].SectionType).toEqual(luSectionTypes.SIMPLEINTENTSECTION); + expect(luresource2.Sections[1].Name).toEqual('CheckEmail'); + expect(luresource2.Sections[1].UtteranceAndEntitiesMap.length).toEqual(3); + expect(luresource2.Sections[1].UtteranceAndEntitiesMap[0].utterance).toEqual('check my email'); + expect(luresource2.Sections[1].UtteranceAndEntitiesMap[1].utterance).toEqual('show my emails 2'); + expect(luresource.Sections[1].UtteranceAndEntitiesMap[1].utterance).toEqual('show my emails'); // do not modify arguments + expect(luresource2.Sections[1].UtteranceAndEntitiesMap[2].utterance).toEqual('check my mail box please'); }); it('update section with syntax error: missing -', () => { @@ -124,13 +149,17 @@ hi `, }; + const luFile1 = luIndexer.parse(validFileContent); + // when intent invalid, after update can still be parsed - const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); + const updatedContent2 = updateIntent(luFile1, intentName, invalidIntent).content; const updatedContent2Parsed = luParser.parse(updatedContent2); expect(updatedContent2Parsed.Sections.length).toEqual(1); expect(updatedContent2Parsed.Errors.length).toBeGreaterThan(0); // when file invalid, update with valid intent should fix error. - const updatedContent3 = updateIntent(updatedContent2, intentName, validIntent); + const luFile2 = luIndexer.parse(updatedContent2); + + const updatedContent3 = updateIntent(luFile2, intentName, validIntent).content; const updatedContent3Parsed = luParser.parse(updatedContent3); expect(updatedContent3Parsed.Sections.length).toEqual(1); expect(updatedContent3Parsed.Errors.length).toEqual(0); @@ -149,9 +178,10 @@ hi - show my emails @`, }; + const luFile1 = luIndexer.parse(validFileContent); // when intent invalid, after update can still be parsed - const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); + const updatedContent2 = updateIntent(luFile1, intentName, invalidIntent).content; const updatedContent2Parsed = luParser.parse(updatedContent2); expect(updatedContent2Parsed.Errors.length).toBeGreaterThan(0); // TODO: update back should fix error. @@ -174,8 +204,10 @@ hi # UnexpectedIntentDefination `, }; + const luFile1 = luIndexer.parse(validFileContent); + // should auto escape # to \# - const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); + const updatedContent2 = updateIntent(luFile1, intentName, invalidIntent).content; const { Sections, Errors } = luParser.parse(updatedContent2); expect(Errors.length).toEqual(0); expect(Sections.length).toEqual(1); @@ -187,7 +219,8 @@ hi it('delete section test', () => { const intentName = 'CheckEmail'; - const fileContentUpdated = removeIntent(fileContent, intentName); + const luFile1 = luIndexer.parse(fileContent, fileId1); + const fileContentUpdated = removeIntent(luFile1, intentName).content; const luresource = luParser.parse(fileContentUpdated); const { Sections, Errors } = luresource; @@ -215,6 +248,7 @@ describe('LU Nested Section CRUD test', () => { - show my deleted todos @ simple todoSubject`; + const fileId = 'a.lu'; it('update IntentSection test', () => { const intentName = 'CheckTodo'; @@ -228,9 +262,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = updateIntent(fileContent, intentName, intent); - const luresource = luParser.parse(fileContentUpdated); - const { Sections, Errors } = luresource; + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = updateIntent(luFile1, intentName, intent); + const result = luParser.parse(luFile1Updated.content); + const { Sections, Errors } = result; expect(Errors.length).toEqual(0); expect(Sections.length).toEqual(2); @@ -270,8 +305,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = addIntent(fileContent, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = addIntent(luFile1, intent); + const luresource = luParser.parse(luFile1Updated.content); + const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -300,8 +337,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = addIntent(fileContent, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = addIntent(luFile1, intent); + const luresource = luParser.parse(luFile1Updated.content); + const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -333,8 +372,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = updateIntent(fileContent, intentName, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = updateIntent(luFile1, intentName, intent); + const luresource = luParser.parse(luFile1Updated.content); + const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -370,8 +411,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = updateIntent(fileContent, intentName, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = updateIntent(luFile1, intentName, intent); + const luresource = luParser.parse(luFile1Updated.content); + const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -400,16 +443,18 @@ describe('LU Nested Section CRUD test', () => { ### Oops `; - const fileContentUpdated1 = updateIntent(fileContent, intentName, { Name: intentName, Body: intentBody1 }); - const luresource1 = luParser.parse(fileContentUpdated1); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated1 = updateIntent(luFile1, intentName, { Name: intentName, Body: intentBody1 }); + const luresource1 = luParser.parse(luFile1Updated1.content); + expect(luresource1.Sections.length).toBeGreaterThan(0); expect(luresource1.Errors.length).toBeGreaterThan(0); const intentBody2 = `## Oops ### Oops `; - const fileContentUpdated2 = updateIntent(fileContent, intentName, { Name: intentName, Body: intentBody2 }); - const luresource2 = luParser.parse(fileContentUpdated2); + const luFile1Updated2 = updateIntent(luFile1, intentName, { Name: intentName, Body: intentBody2 }); + const luresource2 = luParser.parse(luFile1Updated2.content); expect(luresource2.Sections.length).toEqual(2); expect(luresource2.Errors.length).toBeGreaterThan(0); expect(luresource2.Sections[0].SectionType).toEqual(luSectionTypes.MODELINFOSECTION); @@ -425,8 +470,9 @@ describe('LU Nested Section CRUD test', () => { const intentBody3 = `## Oops ### Oops `; - const fileContentUpdated3 = updateIntent(fileContent3, intentName, { Name: intentName, Body: intentBody3 }); - const luresource3 = luParser.parse(fileContentUpdated3); + const luFile2 = luIndexer.parse(fileContent3, fileId); + const luFile1Updated3 = updateIntent(luFile2, intentName, { Name: intentName, Body: intentBody3 }); + const luresource3 = luParser.parse(luFile1Updated3.content); expect(luresource3.Sections.length).toBeGreaterThan(0); expect(luresource3.Errors.length).toBeGreaterThan(0); }); @@ -446,8 +492,10 @@ describe('LU Nested Section CRUD test', () => { `, }; - const fileContentUpdated = updateIntent(fileContent, intentName, intent); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + + const luFile1Updated = updateIntent(luFile1, intentName, intent); + const luresource = luParser.parse(luFile1Updated.content); const { Sections, Errors } = luresource; expect(Errors.length).toEqual(0); @@ -473,8 +521,9 @@ describe('LU Nested Section CRUD test', () => { it('delete nestedIntentSection test', () => { const Name = 'CheckTodo/CheckUnreadTodo'; - const fileContentUpdated = removeIntent(fileContent, Name); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + const luFile1Updated = removeIntent(luFile1, Name); + const luresource = luParser.parse(luFile1Updated.content); const { Sections, Errors } = luresource; @@ -495,16 +544,20 @@ describe('LU Nested Section CRUD test', () => { it('delete nestedIntentSection test, parrent not exist', () => { const Name = 'CheckTodoNotExist/CheckUnreadTodo'; - const fileContentUpdated = removeIntent(fileContent, Name); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + + const luFile1Updated = removeIntent(luFile1, Name); + const luresource = luParser.parse(luFile1Updated.content); const { Content } = luresource; expect(Content).toEqual(fileContent); }); it('delete nestedIntentSection test, child not exist', () => { const Name = 'CheckTodo/CheckUnreadTodoNotExist'; - const fileContentUpdated = removeIntent(fileContent, Name); - const luresource = luParser.parse(fileContentUpdated); + const luFile1 = luIndexer.parse(fileContent, fileId); + + const luFile1Updated = removeIntent(luFile1, Name); + const luresource = luParser.parse(luFile1Updated.content); const { Content } = luresource; expect(Content).toEqual(fileContent); }); diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 7ca30a113c..e7a4e6e50c 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,9 +28,9 @@ }, "dependencies": { "@bfc/shared": "*", - "@microsoft/bf-lu": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-1.3.6.tgz", + "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", "adaptive-expressions": "4.10.0-preview-147186", - "botbuilder-lg": "4.10.0-preview-147186", + "botbuilder-lg": "^4.10.0-preview-150886", "lodash": "^4.17.19" } } \ No newline at end of file diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 553a6e57e3..f77176c762 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -2,94 +2,17 @@ // Licensed under the MIT License. import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; -import get from 'lodash/get'; -import { - FileInfo, - LuFile, - LuParsed, - LuSectionTypes, - LuIntentSection, - Diagnostic, - Position, - Range, - DiagnosticSeverity, -} from '@bfc/shared'; +import { FileInfo, LuFile } from '@bfc/shared'; import { getBaseName } from './utils/help'; import { FileExtensions } from './utils/fileExtensions'; +import { convertLuParseResultToLuFile } from './utils/luUtil'; const { luParser } = sectionHandler; -function convertLuDiagnostic(d: any, source: string): Diagnostic { - const severityMap = { - ERROR: DiagnosticSeverity.Error, - WARN: DiagnosticSeverity.Warning, - INFORMATION: DiagnosticSeverity.Information, - HINT: DiagnosticSeverity.Hint, - }; - const result = new Diagnostic(d.Message, source, severityMap[d.Severity]); - - const start: Position = d.Range ? new Position(d.Range.Start.Line, d.Range.Start.Character) : new Position(0, 0); - const end: Position = d.Range ? new Position(d.Range.End.Line, d.Range.End.Character) : new Position(0, 0); - result.range = new Range(start, end); - - return result; -} - -function parse(content: string, id = ''): LuParsed { - const { Sections, Errors } = luParser.parse(content); - const intents: LuIntentSection[] = []; - Sections.forEach((section) => { - const { Name, Body, SectionType } = section; - const range = { - startLineNumber: get(section, 'ParseTree.start.line', 0), - endLineNumber: get(section, 'ParseTree.stop.line', 0), - }; - if (SectionType === LuSectionTypes.SIMPLEINTENTSECTION) { - const Entities = section.Entities.map(({ Name }) => Name); - intents.push({ - Name, - Body, - Entities, - range, - }); - } else if (SectionType === LuSectionTypes.NESTEDINTENTSECTION) { - const Children = section.SimpleIntentSections.map((subSection) => { - const { Name, Body } = subSection; - const range = { - startLineNumber: get(subSection, 'ParseTree.start.line', 0), - endLineNumber: get(subSection, 'ParseTree.stop.line', 0), - }; - const Entities = subSection.Entities.map(({ Name }) => Name); - return { - Name, - Body, - Entities, - range, - }; - }); - intents.push({ - Name, - Body, - Children, - range, - }); - intents.push( - ...Children.map((subSection) => { - return { - ...subSection, - Name: `${section.Name}/${subSection.Name}`, - }; - }) - ); - } - }); - const diagnostics = Errors.map((e) => convertLuDiagnostic(e, id)); - return { - empty: !Sections.length, - intents, - diagnostics, - }; +function parse(content: string, id = ''): LuFile { + const result = luParser.parse(content); + return convertLuParseResultToLuFile(id, result); } function index(files: FileInfo[]): LuFile[] { @@ -100,7 +23,7 @@ function index(files: FileInfo[]): LuFile[] { const luFiles = filtered.map((file) => { const { name, content } = file; const id = getBaseName(name); - return { id, content, ...parse(content, id) }; + return parse(content, id); }); return luFiles; diff --git a/Composer/packages/lib/indexers/src/utils/lgUtil.ts b/Composer/packages/lib/indexers/src/utils/lgUtil.ts index 6d173a9caa..c6f7dfea4b 100644 --- a/Composer/packages/lib/indexers/src/utils/lgUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/lgUtil.ts @@ -38,8 +38,8 @@ export function convertTemplatesToLgFile(id = '', content: string, templates: Te body: t.body, parameters: t.parameters, range: { - startLineNumber: get(t, 'sourceRange.parseTree.start.line', 0), - endLineNumber: get(t, 'sourceRange.parseTree.stop.line', 0), + startLineNumber: get(t, 'sourceRange.range.start.line', 0), + endLineNumber: get(t, 'sourceRange.range.end.line', 0), }, }; }); @@ -64,12 +64,12 @@ export function increaseNameUtilNotExist(templates: LgTemplate[], name: string): } export function updateTemplate( - id = '', - content: string, + lgFile: LgFile, templateName: string, { name, parameters, body }: { name?: string; parameters?: string[]; body?: string }, importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const originTemplate = resource.toArray().find((t) => t.name === templateName); let templates; @@ -90,22 +90,18 @@ export function updateTemplate( // if name exist, throw error. export function addTemplate( - id = '', - content: string, + lgFile: LgFile, { name, parameters = [], body }: LgTemplate, importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const templates = resource.addTemplate(name, parameters, body); return convertTemplatesToLgFile(id, templates.toString(), templates); } -export function addTemplates( - id = '', - content: string, - templates: LgTemplate[], - importResolver?: ImportResolverDelegate -): LgFile { +export function addTemplates(lgFile: LgFile, templates: LgTemplate[], importResolver?: ImportResolverDelegate): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); for (const { name, parameters = [], body } of templates) { resource.addTemplate(name, parameters, body); @@ -115,11 +111,11 @@ export function addTemplates( // if name exist, add it anyway, with name like `${name}1` `${name}2` export function addTemplateAnyway( - id = '', - content: string, + lgFile: LgFile, { name = 'TemplateName', parameters = [], body = '-TemplateBody' }: LgTemplate, importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const newName = increaseNameUtilNotExist(resource.toArray(), name); @@ -129,12 +125,12 @@ export function addTemplateAnyway( // if toTemplateName exist, throw error. export function copyTemplate( - id = '', - content: string, + lgFile: LgFile, fromTemplateName: string, toTemplateName: string, importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const fromTemplate = resource.toArray().find((t) => t.name === fromTemplateName); if (!fromTemplate) { @@ -147,12 +143,12 @@ export function copyTemplate( // if toTemplateName exist, add it anyway, with name like `${toTemplateName}1` `${toTemplateName}2` export function copyTemplateAnyway( - id = '', - content: string, + lgFile: LgFile, fromTemplateName: string, toTemplateName?: string, importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const fromTemplate = resource.toArray().find((t) => t.name === fromTemplateName); if (!fromTemplate) { @@ -169,23 +165,19 @@ export function copyTemplateAnyway( return convertTemplatesToLgFile(id, templates.toString(), templates); } -export function removeTemplate( - id = '', - content: string, - templateName: string, - importResolver?: ImportResolverDelegate -): LgFile { +export function removeTemplate(lgFile: LgFile, templateName: string, importResolver?: ImportResolverDelegate): LgFile { + const { id, content } = lgFile; const resource = Templates.parseText(content, undefined, importResolver); const templates = resource.deleteTemplate(templateName); return convertTemplatesToLgFile(id, templates.toString(), templates); } export function removeTemplates( - id = '', - content: string, + lgFile: LgFile, templateNames: string[], importResolver?: ImportResolverDelegate ): LgFile { + const { id, content } = lgFile; let resource = Templates.parseText(content, undefined, importResolver); templateNames.forEach((templateName) => { resource = resource.deleteTemplate(templateName); diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index 61c0736736..2bef9426ca 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -3,17 +3,16 @@ /** * luUtil.ts is a single place use lu-parser handle lu file operation. + * Those methods should be implemented as pure function, shall not modify arguments. * it's designed have no state, input text file, output text file. * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ - import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import isEmpty from 'lodash/isEmpty'; -import { LuIntentSection, LuSectionTypes, Diagnostic } from '@bfc/shared'; +import get from 'lodash/get'; +import { LuFile, LuSectionTypes, LuIntentSection, Diagnostic, Position, Range, DiagnosticSeverity } from '@bfc/shared'; import formatMessage from 'format-message'; -import { luIndexer } from '../luIndexer'; - import { buildNewlineText, splitNewlineText } from './help'; const { luParser, sectionOperator } = sectionHandler; @@ -23,6 +22,67 @@ const NEWLINE = '\r\n'; // when new add a section in inline editor, the section haven't exist on file context, to make suggestion/validation possiable here mock one. export const PlaceHolderSectionName = formatMessage(`_NewSectionPlaceHolderSectionName`); +export function convertLuDiagnostic(d: any, source: string): Diagnostic { + const severityMap = { + ERROR: DiagnosticSeverity.Error, + WARN: DiagnosticSeverity.Warning, + INFORMATION: DiagnosticSeverity.Information, + HINT: DiagnosticSeverity.Hint, + }; + const result = new Diagnostic(d.Message, source, severityMap[d.Severity]); + + const start: Position = d.Range ? new Position(d.Range.Start.Line, d.Range.Start.Character) : new Position(0, 0); + const end: Position = d.Range ? new Position(d.Range.End.Line, d.Range.End.Character) : new Position(0, 0); + result.range = new Range(start, end); + + return result; +} + +export function convertLuParseResultToLuFile(id = '', resource): LuFile { + // filter structured-object from LUParser result. + const { Sections, Errors, Content } = resource; + const intents: LuIntentSection[] = []; + Sections.forEach((section) => { + const { Name, Body, SectionType } = section; + const range = { + startLineNumber: get(section, 'Range.Start.Line', 0), + endLineNumber: get(section, 'Range.End.Line', 0), + }; + if (SectionType === LuSectionTypes.SIMPLEINTENTSECTION) { + const Entities = section.Entities.map(({ Name }) => Name); + intents.push({ Name, Body, Entities, range }); + } else if (SectionType === LuSectionTypes.NESTEDINTENTSECTION) { + const Children = section.SimpleIntentSections.map((subSection) => { + const { Name, Body } = subSection; + const range = { + startLineNumber: get(subSection, 'Range.Start.Line', 0), + endLineNumber: get(subSection, 'Range.End.Line', 0), + }; + const Entities = subSection.Entities.map(({ Name }) => Name); + return { Name, Body, Entities, range }; + }); + intents.push({ Name, Body, Children, range }); + intents.push( + ...Children.map((subSection) => { + return { + ...subSection, + Name: `${section.Name}/${subSection.Name}`, + }; + }) + ); + } + }); + const diagnostics = Errors.map((e) => convertLuDiagnostic(e, id)); + return { + id, + content: Content, + empty: !Sections.length, + intents, + diagnostics, + resource: { Sections, Errors, Content }, + }; +} + export function isValid(diagnostics: any[]) { return diagnostics.every((item) => { item.Severity !== 'ERROR'; @@ -70,7 +130,9 @@ export function textFromIntents(intents: LuIntentSection[], nestedLevel = 1): st export function checkSection(intent: LuIntentSection, enableSections = true): Diagnostic[] { const text = textFromIntent(intent, 1, enableSections); - return luIndexer.parse(text).diagnostics; + const result = luParser.parse(text); + const { Errors } = result; + return Errors.map((e) => convertLuDiagnostic(e, '')); } export function checkIsSingleSection(intent: LuIntentSection, enableSections = true): boolean { @@ -108,11 +170,12 @@ function updateInSections( * @param intentName intent Name, support subSection naming 'CheckEmail/CheckUnreadEmail'. if #CheckEmail not exist will do recursive add. * @param {Name, Body} intent the updates. if intent is empty will do remove. */ -export function updateIntent(content: string, intentName: string, intent: LuIntentSection | null): string { +export function updateIntent(luFile: LuFile, intentName: string, intent: LuIntentSection | null): LuFile { let targetSection; let targetSectionContent; + const { id, resource } = luFile; + const updatedSectionContent = textFromIntent(intent); - const resource = luParser.parse(content); const { Sections } = resource; // if intent is null, do remove // and if remove target not exist return origin content; @@ -123,14 +186,15 @@ export function updateIntent(content: string, intentName: string, intent: LuInte ({ Name }) => Name === childName ); if (!targetChildSection) { - return content; + return luFile; } } else { const targetSection = Sections.find(({ Name }) => Name === intentName); if (targetSection) { - return new sectionOperator(resource).deleteSection(targetSection.Id).Content; + const result = new sectionOperator(resource).deleteSection(targetSection.Id); + return convertLuParseResultToLuFile(id, result); } - return content; + return luFile; } } @@ -150,13 +214,15 @@ export function updateIntent(content: string, intentName: string, intent: LuInte targetSectionContent = updatedSectionContent; } + let newResource; // update if (targetSection) { - return new sectionOperator(resource).updateSection(targetSection.Id, targetSectionContent).Content; + newResource = new sectionOperator(resource).updateSection(targetSection.Id, targetSectionContent); // add if not exist } else { - return new sectionOperator(resource).addSection(['', targetSectionContent].join(NEWLINE)).Content; + newResource = new sectionOperator(resource).addSection(['', targetSectionContent].join(NEWLINE)); } + return convertLuParseResultToLuFile(id, newResource); } /** @@ -164,18 +230,18 @@ export function updateIntent(content: string, intentName: string, intent: LuInte * @param content origin lu file content * @param {Name, Body} intent the adds. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if #CheckEmail not exist will do recursive add. */ -export function addIntent(content: string, { Name, Body, Entities }: LuIntentSection): string { +export function addIntent(luFile: LuFile, { Name, Body, Entities }: LuIntentSection): LuFile { const intentName = Name; if (Name.includes('/')) { const [, childName] = Name.split('/'); Name = childName; } // If the invoker doesn't want to carry Entities, don't pass Entities in. - return updateIntent(content, intentName, { Name, Body, Entities }); + return updateIntent(luFile, intentName, { Name, Body, Entities }); } -export function addIntents(content: string, intents: LuIntentSection[]): string { - let result = content; +export function addIntents(luFile: LuFile, intents: LuIntentSection[]): LuFile { + let result = luFile; for (const intent of intents) { result = addIntent(result, intent); } @@ -187,26 +253,18 @@ export function addIntents(content: string, intents: LuIntentSection[]): string * @param content origin lu file content * @param intentName the remove intentName. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if any of them not exist will do nothing. */ -export function removeIntent(content: string, intentName: string): string { - if (intentName.includes('/')) { - return updateIntent(content, intentName, null); - } - const resource = luParser.parse(content); - const { Sections } = resource; - const targetSection = Sections.find(({ Name }) => Name === intentName); - if (targetSection) { - return new sectionOperator(resource).deleteSection(targetSection.Id).Content; - } - return content; +export function removeIntent(luFile: LuFile, intentName: string): LuFile { + return updateIntent(luFile, intentName, null); } -export function removeIntents(content: string, intentNames: string[]): string { - let result = content; +export function removeIntents(luFile: LuFile, intentNames: string[]): LuFile { + let result = luFile; for (const intentName of intentNames) { result = removeIntent(result, intentName); } return result; } -export function parse(id: string, content: string) { - return { id, content, ...luIndexer.parse(content, id) }; +export function parse(id: string, content: string): LuFile { + const result = luParser.parse(content); + return convertLuParseResultToLuFile(id, result); } diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 4fcffa19bb..051f1ac99f 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -87,45 +87,154 @@ const initialDialogShape = () => ({ }, [SDKKinds.OnChooseIntent]: { $kind: SDKKinds.OnChooseIntent, + $designer: { + id: generateDesignerId(), + }, actions: [ { $kind: SDKKinds.SetProperty, $designer: { id: generateDesignerId(), }, - property: 'dialog.recognized', - value: '=turn.recognized', + assignments: [ + { + value: '=turn.recognized.candidates[0]', + property: 'dialog.luisResult', + }, + { + property: 'dialog.qnaResult', + value: '=turn.recognized.candidates[1]', + }, + ], + }, + { + $kind: SDKKinds.IfCondition, + $designer: { + id: generateDesignerId(), + }, + condition: 'dialog.luisResult.score >= 0.9 && dialog.qnaResult.score <= 0.5', + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'recognizedIntent', + eventValue: '=dialog.luisResult.result', + disabled: true, + }, + { + $kind: SDKKinds.BreakLoop, + $designer: { + id: generateDesignerId(), + }, + disabled: true, + }, + ], + disabled: true, + }, + { + $kind: SDKKinds.IfCondition, + $designer: { + id: generateDesignerId(), + }, + condition: 'dialog.luisResult.score <= 0.5 && dialog.qnaResult.score >= 0.9', + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'recognizedIntent', + eventValue: '=dialog.qnaResult.result', + disabled: true, + }, + { + $kind: SDKKinds.BreakLoop, + $designer: { + id: generateDesignerId(), + }, + disabled: true, + }, + ], + disabled: true, }, { - $kind: SDKKinds.ChoiceInput, + $kind: SDKKinds.IfCondition, + $designer: { + id: generateDesignerId(), + }, + condition: 'dialog.qnaResult.score <= 0.05', + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'recognizedIntent', + eventValue: '=dialog.luisResult.result', + disabled: true, + }, + { + $kind: SDKKinds.BreakLoop, + $designer: { + id: generateDesignerId(), + }, + disabled: true, + }, + ], + top: 3, + disabled: true, + cardNoMatchResponse: 'Thanks for the feedback.', + cardNoMatchText: 'None of the above.', + }, + { + $kind: SDKKinds.TextInput, $designer: { id: generateDesignerId(), }, - defaultLocale: 'en-us', disabled: false, maxTurnCount: 3, - alwaysPrompt: false, + alwaysPrompt: true, allowInterruptions: false, prompt: '', - choiceOptions: { - includeNumbers: true, - inlineOrMore: ', or ', - inlineOr: ' or ', - inlineSeparator: ', ', - }, - property: 'dialog.choice', - choices: '=foreach(dialog.recognized.candidates, x, x.intent)', - outputFormat: 'index', + property: 'turn.intentChoice', + value: '=@userChosenIntent', top: 3, cardNoMatchResponse: 'Thanks for the feedback.', + cardNoMatchText: 'None of the above.', + activeLearningCardTitle: 'Did you mean:', + threshold: 0.3, + noAnswer: 'Sorry, I did not find an answer.', + hostname: '=settings.qna.hostname', + endpointKey: '=settings.qna.endpointkey', + knowledgeBaseId: '=settings.qna.knowledgebaseid', }, { - $kind: SDKKinds.EmitEvent, + $kind: SDKKinds.IfCondition, $designer: { id: generateDesignerId(), }, - eventValue: '=dialog.recognized.candidates[dialog.choice].result', - eventName: 'recognizedIntent', + condition: "turn.intentChoice != 'none'", + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'recognizedIntent', + eventValue: '=dialog[turn.intentChoice].result', + }, + ], + elseActions: [ + { + $kind: SDKKinds.SendActivity, + $designer: { + id: generateDesignerId(), + }, + activity: '', + }, + ], top: 3, cardNoMatchResponse: 'Thanks for the feedback.', cardNoMatchText: 'None of the above.', diff --git a/Composer/packages/lib/shared/src/types/EditorAPI.ts b/Composer/packages/lib/shared/src/types/EditorAPI.ts new file mode 100644 index 0000000000..0a458faf08 --- /dev/null +++ b/Composer/packages/lib/shared/src/types/EditorAPI.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +type EditorAPIHandler = () => any; + +export interface EditorAPI { + Editing: { + Undo: EditorAPIHandler; + Redo: EditorAPIHandler; + }; + Actions: { + CopySelection: EditorAPIHandler; + CutSelection: EditorAPIHandler; + MoveSelection: EditorAPIHandler; + DeleteSelection: EditorAPIHandler; + DisableSelection: EditorAPIHandler; + EnableSelection: EditorAPIHandler; + }; +} + +const EmptyHandler = () => null; + +const DefaultEditorAPI: EditorAPI = { + Editing: { + Undo: EmptyHandler, + Redo: EmptyHandler, + }, + Actions: { + CopySelection: EmptyHandler, + CutSelection: EmptyHandler, + MoveSelection: EmptyHandler, + DeleteSelection: EmptyHandler, + DisableSelection: EmptyHandler, + EnableSelection: EmptyHandler, + }, +}; + +const EDITOR_API_NAME = 'EditorAPI'; + +export function getEditorAPI(): EditorAPI { + if (!window[EDITOR_API_NAME]) { + window[EDITOR_API_NAME] = { ...DefaultEditorAPI }; + } + return window[EDITOR_API_NAME]; +} + +export function registerEditorAPI(domain: 'Editing' | 'Actions', handlers: { [fn: string]: EditorAPIHandler }) { + const editorAPI: EditorAPI = getEditorAPI(); + + // reject unrecognized api domain. + if (!editorAPI[domain]) return; + + const domainAPIs = editorAPI[domain]; + const overridedAPIs = Object.keys(handlers) + .filter((fnName) => !!domainAPIs[fnName]) + .reduce((results, fnName) => { + results[fnName] = handlers[fnName]; + return results; + }, {}); + + const newDomainAPIs = { + ...domainAPIs, + ...overridedAPIs, + }; + editorAPI[domain] = newDomainAPIs as any; +} diff --git a/Composer/packages/lib/shared/src/types/index.ts b/Composer/packages/lib/shared/src/types/index.ts index 77dd246431..ab3827c46c 100644 --- a/Composer/packages/lib/shared/src/types/index.ts +++ b/Composer/packages/lib/shared/src/types/index.ts @@ -9,3 +9,4 @@ export * from './sdk'; export * from './settings'; export * from './server'; export * from './shell'; +export * from './EditorAPI'; diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 36db42c499..9d2bb000c5 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -61,7 +61,7 @@ "@bfc/plugin-loader": "*", "@bfc/shared": "*", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", - "@microsoft/bf-lu": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.2.tgz", + "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", "archiver": "^3.0.0", "axios": "^0.19.2", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/models/bot/builder.ts b/Composer/packages/server/src/models/bot/builder.ts index 7035b2dcf3..990e61dfea 100644 --- a/Composer/packages/server/src/models/bot/builder.ts +++ b/Composer/packages/server/src/models/bot/builder.ts @@ -214,6 +214,7 @@ export class Builder { config.botName, config.suffix, config.fallbackLocal, + true, false, false, loadResult.multiRecognizers, diff --git a/Composer/packages/tools/language-servers/language-generation/package.json b/Composer/packages/tools/language-servers/language-generation/package.json index 78cee92ee4..5d102abca0 100644 --- a/Composer/packages/tools/language-servers/language-generation/package.json +++ b/Composer/packages/tools/language-servers/language-generation/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@bfc/indexers": "*", - "botbuilder-lg": "4.10.0-preview-147186", + "botbuilder-lg": "^4.10.0-preview-150886", "vscode-languageserver": "^5.3.0-next" }, "devDependencies": { diff --git a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts index 77a3b3d9e5..1e97a25c2e 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts @@ -15,9 +15,8 @@ import { } from 'vscode-languageserver-types'; import { TextDocumentPositionParams, DocumentOnTypeFormattingParams } from 'vscode-languageserver-protocol'; import get from 'lodash/get'; -import { filterTemplateDiagnostics, isValid } from '@bfc/indexers'; -import { MemoryResolver, ResolverResource, LgTemplate } from '@bfc/shared'; -import * as lgUtil from '@bfc/indexers/lib/utils/lgUtil'; +import { filterTemplateDiagnostics, isValid, lgUtil } from '@bfc/indexers'; +import { MemoryResolver, ResolverResource, LgFile } from '@bfc/shared'; import { LgParser } from './lgParser'; import { buildInfunctionsMap } from './builtinFunctionsMap'; @@ -159,30 +158,19 @@ export class LGServer { protected addLGDocument(document: TextDocument, lgOption?: LGOption) { const { uri } = document; const { fileId, templateId, projectId } = lgOption || {}; - const index = async () => { - let content = this.documents.get(uri)?.getText() || ''; + const index = (): LgFile => { + const content = this.documents.get(uri)?.getText() || ''; // if inline mode, composite local with server resolved file. - if (this.getLgResources && fileId && templateId) { - const resources = this.getLgResources(projectId); - const lastContent = resources.find((item) => item.id === fileId)?.content; - - if (lastContent) { - const body = content; - content = lgUtil.updateTemplate('', lastContent, templateId, { body }).content; + const lgTextFiles = projectId ? this.getLgResources(projectId) : []; + if (fileId && templateId) { + const lgTextFile = lgTextFiles.find((item) => item.id === fileId); + if (lgTextFile) { + const lgFile = lgUtil.parse(lgTextFile.id, lgTextFile.content, lgTextFiles); + return lgUtil.updateTemplate(lgFile, templateId, { body: content }); } } - const id = fileId || uri; - let templates: LgTemplate[] = []; - let diagnostics: any[] = []; - try { - const payload = await this._lgParser.parseText(content, id, this.getLgResources(projectId)); - templates = payload.templates; - diagnostics = payload.diagnostics; - } catch (error) { - diagnostics.push(generageDiagnostic(error.message, DiagnosticSeverity.Error, document)); - } - return { id, content, templates, diagnostics }; + return lgUtil.parse(fileId || uri, content, lgTextFiles); }; const lgDocument: LGDocument = { uri, @@ -203,7 +191,7 @@ export class LGServer { if (!document) { return Promise.resolve(null); } - const lgFile = await this.getLGDocument(document)?.index(); + const lgFile = this.getLGDocument(document)?.index(); if (!lgFile) { return Promise.resolve(null); } @@ -477,7 +465,7 @@ export class LGServer { const wordAtCurRange = document.getText(range); const endWithDot = wordAtCurRange.endsWith('.'); const lgDoc = this.getLGDocument(document); - const lgFile = await lgDoc?.index(); + const lgFile = lgDoc?.index(); const templateId = lgDoc?.templateId; const lines = document.getText(range).split('\n'); if (!lgFile) { @@ -700,7 +688,7 @@ export class LGServer { return; } const { fileId, templateId, uri, projectId } = lgDoc; - const lgFile = await lgDoc.index(); + const lgFile = lgDoc.index(); if (!lgFile) { return; } @@ -737,7 +725,11 @@ export class LGServer { } let lgDiagnostics: any[] = []; try { - const payload = await this._lgParser.parseText(text, fileId || uri, this.getLgResources(projectId)); + const payload = await this._lgParser.parseText( + text, + fileId || uri, + projectId ? this.getLgResources(projectId) : [] + ); lgDiagnostics = payload.diagnostics; } catch (error) { lgDiagnostics.push(generageDiagnostic(error.message, DiagnosticSeverity.Error, document)); diff --git a/Composer/packages/tools/language-servers/language-generation/src/utils.ts b/Composer/packages/tools/language-servers/language-generation/src/utils.ts index 5faed76d55..ec48be8c21 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/utils.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/utils.ts @@ -36,7 +36,7 @@ export interface LGDocument { projectId?: string; fileId?: string; templateId?: string; - index: () => Promise; + index: () => LgFile; } export type LGFileResolver = (id: string) => LgFile | undefined; diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index 4ac60bcec8..93493aa6a4 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@microsoft/bf-cli-command": "^4.10.0-preview.141651", - "@microsoft/bf-lu": "^4.10.0-preview.141651", + "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", "express": "^4.15.2", "monaco-languageclient": "^0.10.0", "normalize-url": "^2.0.1", diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts index 0b38e28bf6..10374312c4 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts @@ -135,12 +135,13 @@ export class LUServer { if (this.importResolver && fileId && projectId) { const resolver = this.importResolver; return (source: string, id: string) => { - const luFile = resolver(source, id, projectId); - if (!luFile) { + const plainLuFile = resolver(source, id, projectId); + if (!plainLuFile) { this.sendDiagnostics(document, [ generageDiagnostic(`lu file: ${fileId}.lu not exist on server`, DiagnosticSeverity.Error, document), ]); } + const luFile = luIndexer.parse(plainLuFile.content, plainLuFile.id); let { content } = luFile; /** * source is . means use as file resolver, not import resolver @@ -148,7 +149,7 @@ export class LUServer { * so here build the full content from server file content and editor content */ if (source === '.' && sectionId) { - content = updateIntent(luFile.content, sectionId, { Name: sectionId, Body: editorContent }); + content = updateIntent(luFile, sectionId, { Name: sectionId, Body: editorContent }).content; } return { id, content }; }; diff --git a/Composer/plugins/lib/bot-deploy/package.json b/Composer/plugins/lib/bot-deploy/package.json index 9e5b9c110c..1e6e7759cb 100644 --- a/Composer/plugins/lib/bot-deploy/package.json +++ b/Composer/plugins/lib/bot-deploy/package.json @@ -20,8 +20,8 @@ "@azure/graph": "^5.0.1", "@azure/ms-rest-browserauth": "^0.1.4", "@azure/ms-rest-nodeauth": "^3.0.3", - "@microsoft/bf-lu": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.0.tgz", - "@microsoft/bf-luis-cli": "^4.10.0-preview.141651", + "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", + "@microsoft/bf-luis-cli": "^4.10.0-dev.20200721.8bb21ac", "@types/archiver": "^3.1.0", "@types/fs-extra": "^8.1.0", "@types/request": "^2.48.4", diff --git a/Composer/plugins/lib/bot-deploy/src/botProjectDeploy.ts b/Composer/plugins/lib/bot-deploy/src/botProjectDeploy.ts index 953f9698db..32b4edfbec 100644 --- a/Composer/plugins/lib/bot-deploy/src/botProjectDeploy.ts +++ b/Composer/plugins/lib/bot-deploy/src/botProjectDeploy.ts @@ -18,6 +18,7 @@ import { GraphRbacManagementClient } from '@azure/graph'; import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth'; import * as fs from 'fs-extra'; import * as rp from 'request-promise'; +import { FileInfo } from '@bfc/shared'; import { BotProjectDeployConfig } from './botProjectDeployConfig'; import { BotProjectDeployLoggerType } from './botProjectLoggerType'; @@ -424,13 +425,18 @@ export class BotProjectDeploy { content: fs.readJSONSync(dialog), }); } - luFiles = luFiles.map((luFile) => luFile.substring(luFile.lastIndexOf('\\') + 1)); - this.crossTrainConfig = createCrossTrainConfig(dialogs, luFiles); - } - private needCrossTrain() { - return this.crossTrainConfig.rootIds.length > 0; + const luFileInfos: FileInfo[] = luFiles.map((luFile) => { + const fileStats = fs.statSync(luFile); + return { + name: luFile.substring(luFile.lastIndexOf('\\') + 1), + content: fs.readFileSync(luFile, 'utf-8'), + lastModified: fileStats.mtime.toString(), + path: luFile, + relativePath: luFile.substring(luFile.lastIndexOf(this.remoteBotPath) + 1), + }; + }); + this.crossTrainConfig = createCrossTrainConfig(dialogs, luFileInfos); } - private async writeCrossTrainFiles(crossTrainResult) { if (!(await fs.pathExists(this.interruptionFolderPath))) { await fs.mkdir(this.interruptionFolderPath); @@ -443,7 +449,6 @@ export class BotProjectDeploy { } private async crossTrain(luFiles: string[], qnaFiles: string[]) { - if (!this.needCrossTrain()) return; const luContents: { [key: string]: any }[] = []; const qnaContents: { [key: string]: any }[] = []; for (const luFile of luFiles) { @@ -465,7 +470,6 @@ export class BotProjectDeploy { } private async cleanCrossTrain() { - if (!this.needCrossTrain()) return; fs.rmdirSync(this.interruptionFolderPath, { recursive: true }); } private async getInterruptionFiles() { diff --git a/Composer/plugins/lib/bot-deploy/src/utils/crossTrainUtil.ts b/Composer/plugins/lib/bot-deploy/src/utils/crossTrainUtil.ts index cc42eb60cd..471c457a79 100644 --- a/Composer/plugins/lib/bot-deploy/src/utils/crossTrainUtil.ts +++ b/Composer/plugins/lib/bot-deploy/src/utils/crossTrainUtil.ts @@ -7,7 +7,8 @@ * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ import keys from 'lodash/keys'; -import { LuFile, DialogInfo, IIntentTrigger, FieldNames, SDKKinds } from '@bfc/shared'; +import { LuFile, DialogInfo, IIntentTrigger, FieldNames, SDKKinds, FileInfo } from '@bfc/shared'; +import { luIndexer } from '@bfc/indexers'; import { getBaseName, getExtension } from './fileUtil'; import { VisitorFunc, JsonWalk } from './jsonWalk'; @@ -68,8 +69,8 @@ function createConfigId(fileId) { return `${fileId}.lu`; } -function getLuFilesByDialogId(dialogId: string, luFiles: string[]) { - return luFiles.filter((lu) => getBaseName(lu) === dialogId).map((lu) => createConfigId(lu)); +function getLuFilesByDialogId(dialogId: string, luFiles: LuFile[]) { + return luFiles.filter((lu) => getBaseName(lu.id) === dialogId).map((lu) => createConfigId(lu.id)); } function getFileLocale(fileName: string) { @@ -78,7 +79,7 @@ function getFileLocale(fileName: string) { } //replace the dialogId with luFile's name -function addLocaleToConfig(config: ICrossTrainConfig, luFiles: string[]) { +function addLocaleToConfig(config: ICrossTrainConfig, luFiles: LuFile[]) { const { rootIds, triggerRules } = config; config.rootIds = rootIds.reduce((result: string[], id: string) => { return [...result, ...getLuFilesByDialogId(id, luFiles)]; @@ -152,7 +153,7 @@ export interface ICrossTrainConfig { verbose: true } */ -export function createCrossTrainConfig(dialogs: any[], luFiles: string[]): ICrossTrainConfig { +export function createCrossTrainConfig(dialogs: any[], luFilesInfo: FileInfo[]): ICrossTrainConfig { const triggerRules = {}; const countMap = {}; const wrapDialogs: { [key: string]: any }[] = []; @@ -160,10 +161,11 @@ export function createCrossTrainConfig(dialogs: any[], luFiles: string[]): ICros wrapDialogs.push(parse(dialog)); } - luFiles = luFiles.map((luFile) => getBaseName(luFile)); + const luFiles = luIndexer.index(luFilesInfo); + //map all referred lu files luFiles.forEach((file) => { - countMap[getBaseName(file)] = 1; + countMap[getBaseName(file.id)] = 1; }); let rootId = ''; @@ -174,7 +176,7 @@ export function createCrossTrainConfig(dialogs: any[], luFiles: string[]): ICros botName = dialog.content.$designer.name; } - if (luFiles.find((luFile) => getBaseName(luFile) === dialog.luFile)) { + if (luFiles.find((luFile) => getBaseName(luFile.id) === dialog.luFile)) { const { intentTriggers } = dialog; const fileId = dialog.id; //find the trigger's dialog that use a recognizer diff --git a/Composer/plugins/yarn.lock b/Composer/plugins/yarn.lock index a8b6d608a7..4739afba37 100644 --- a/Composer/plugins/yarn.lock +++ b/Composer/plugins/yarn.lock @@ -181,10 +181,10 @@ passport "^0.4.1" path-to-regexp "^6.1.0" -"@microsoft/bf-cli-command@4.10.0-preview.141651": - version "4.10.0-preview.141651" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-4.10.0-preview.141651.tgz#680875f716285fb8658da8098a0ee524b07c5765" - integrity sha1-aAh19xYoX7hljagJig7lJLB8V2U= +"@microsoft/bf-cli-command@4.10.0-dev.20200721.8bb21ac": + version "4.10.0-dev.20200721.8bb21ac" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-4.10.0-dev.20200721.8bb21ac.tgz#9c81b37bc10072ca6beec7d7f68aa309f8a552ec" + integrity sha1-nIGze8EAcspr7sfX9oqjCfilUuw= dependencies: "@oclif/command" "~1.5.19" "@oclif/config" "~1.13.3" @@ -196,10 +196,10 @@ fs-extra "^7.0.1" tslib "~1.10.0" -"@microsoft/bf-lu@4.10.0-preview.141651": - version "4.10.0-preview.141651" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-preview.141651.tgz#29ed2af803d23ee760354913f5b814873bc1285c" - integrity sha1-Ke0q+APSPudgNUkT9bgUhzvBKFw= +"@microsoft/bf-lu@4.10.0-dev.20200721.8bb21ac": + version "4.10.0-dev.20200721.8bb21ac" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200721.8bb21ac.tgz#15c0121fb9e1bf57c18b96de9f4ebcfaeae841fb" + integrity sha1-FcASH7nhv1fBi5ben068+uroQfs= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -215,14 +215,15 @@ get-stdin "^6.0.0" globby "^10.0.1" intercept-stdout "^0.1.2" - lodash "^4.17.15" + lodash "^4.17.19" node-fetch "~2.6.0" semver "^5.5.1" tslib "^1.10.0" -"@microsoft/bf-lu@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.0.tgz": - version "1.4.0" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.0.tgz#73314a85637c3eae98aed59fe3c6042b2186f1ae" +"@microsoft/bf-lu@^4.10.0-dev.20200728.930f45e": + version "4.10.0-dev.20200728.930f45e" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200728.930f45e.tgz#d8a5867d1e3028124b9e0171877b1f063b2bd08e" + integrity sha1-2KWGfR4wKBJLngFxh3sfBjsr0I4= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -238,21 +239,21 @@ get-stdin "^6.0.0" globby "^10.0.1" intercept-stdout "^0.1.2" - lodash "^4.17.15" + lodash "^4.17.19" node-fetch "~2.6.0" semver "^5.5.1" tslib "^1.10.0" -"@microsoft/bf-luis-cli@^4.10.0-preview.141651": - version "4.10.0-preview.141651" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-luis-cli/-/@microsoft/bf-luis-cli-4.10.0-preview.141651.tgz#29ef283f23d9b59841faff4872cb6900b91ec2e8" - integrity sha1-Ke8oPyPZtZhB+v9IcstpALkewug= +"@microsoft/bf-luis-cli@^4.10.0-dev.20200721.8bb21ac": + version "4.10.0-dev.20200721.8bb21ac" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-luis-cli/-/@microsoft/bf-luis-cli-4.10.0-dev.20200721.8bb21ac.tgz#b952806f8093dd1639e1141ede9d27ec74b6d379" + integrity sha1-uVKAb4CT3RY54RQe3p0n7HS203k= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/cognitiveservices-luis-runtime" "5.0.0" "@azure/ms-rest-azure-js" "2.0.1" - "@microsoft/bf-cli-command" "4.10.0-preview.141651" - "@microsoft/bf-lu" "4.10.0-preview.141651" + "@microsoft/bf-cli-command" "4.10.0-dev.20200721.8bb21ac" + "@microsoft/bf-lu" "4.10.0-dev.20200721.8bb21ac" "@oclif/command" "~1.5.19" "@oclif/config" "~1.13.3" "@oclif/errors" "~1.2.2" @@ -1777,7 +1778,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 7d7c8053c0..28fb96dabd 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2929,7 +2929,7 @@ argparse "~1.0.10" tslib "^1.10.0" -"@microsoft/bf-lu@4.10.0-preview.141651", "@microsoft/bf-lu@^4.10.0-preview.141651": +"@microsoft/bf-lu@4.10.0-preview.141651": version "4.10.0-preview.141651" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-preview.141651.tgz#29ed2af803d23ee760354913f5b814873bc1285c" integrity sha1-Ke0q+APSPudgNUkT9bgUhzvBKFw= @@ -2953,9 +2953,10 @@ semver "^5.5.1" tslib "^1.10.0" -"@microsoft/bf-lu@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.2.tgz": - version "1.4.2" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.2.tgz#b078f991a65b91471fe667d5571bf5b8bc92d686" +"@microsoft/bf-lu@^4.10.0-dev.20200728.930f45e": + version "4.10.0-dev.20200728.930f45e" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200728.930f45e.tgz#d8a5867d1e3028124b9e0171877b1f063b2bd08e" + integrity sha1-2KWGfR4wKBJLngFxh3sfBjsr0I4= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -2971,30 +2972,7 @@ get-stdin "^6.0.0" globby "^10.0.1" intercept-stdout "^0.1.2" - lodash "^4.17.15" - node-fetch "~2.6.0" - semver "^5.5.1" - tslib "^1.10.0" - -"@microsoft/bf-lu@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-1.3.6.tgz": - version "1.3.6" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-1.3.6.tgz#69d09da1444a2db06a95d74c9c2fa4599af28f3c" - dependencies: - "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" - "@azure/ms-rest-azure-js" "2.0.1" - "@oclif/command" "~1.5.19" - "@oclif/errors" "~1.2.2" - "@types/node-fetch" "~2.5.5" - antlr4 "^4.7.2" - chalk "2.4.1" - console-stream "^0.1.1" - deep-equal "^1.0.1" - delay "^4.3.0" - fs-extra "^8.1.0" - get-stdin "^6.0.0" - globby "^10.0.1" - intercept-stdout "^0.1.2" - lodash "^4.17.15" + lodash "^4.17.19" node-fetch "~2.6.0" semver "^5.5.1" tslib "^1.10.0" @@ -3677,6 +3655,13 @@ dependencies: moment ">=2.14.0" +"@types/moment-timezone@^0.5.13": + version "0.5.13" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/moment-timezone/-/@types/moment-timezone-0.5.13.tgz#0317ccc91eb4c7f4901704166166395c39276528" + integrity sha1-AxfMyR60x/SQFwQWYWY5XDknZSg= + dependencies: + moment ">=2.14.0" + "@types/morgan@^1.7.35": version "1.7.35" resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.7.35.tgz#6358f502931cc2583d7a94248c41518baa688494" @@ -4459,6 +4444,25 @@ adaptive-expressions@4.10.0-preview-147186: moment "^2.25.1" moment-timezone "^0.5.28" +adaptive-expressions@4.10.0-preview-150886: + version "4.10.0-preview-150886" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/adaptive-expressions/-/adaptive-expressions-4.10.0-preview-150886.tgz#27d147b8b9389318e1b608601d83f5003701cf7b" + integrity sha1-J9FHuLk4kxjhtghgHYP1ADcBz3s= + dependencies: + "@microsoft/recognizers-text-data-types-timex-expression" "1.1.4" + "@types/atob-lite" "^2.0.0" + "@types/lru-cache" "^5.1.0" + "@types/moment-timezone" "^0.5.13" + "@types/xmldom" "^0.1.29" + antlr4ts "0.5.0-alpha.3" + atob-lite "^2.0.0" + big-integer "^1.6.48" + jspath "^0.4.0" + lodash "^4.17.19" + lru-cache "^5.1.1" + moment "^2.25.1" + moment-timezone "^0.5.28" + address@1.0.3, address@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" @@ -5536,15 +5540,16 @@ boolean@^3.0.0: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.1.tgz#35ecf2b4a2ee191b0b44986f14eb5f052a5cbb4f" integrity sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA== -botbuilder-lg@4.10.0-preview-147186: - version "4.10.0-preview-147186" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.10.0-preview-147186.tgz#43ae2290d5264e9920008155a06abb581b5d99aa" +botbuilder-lg@^4.10.0-preview-150886: + version "4.10.0-preview-150886" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.10.0-preview-150886.tgz#bbc91011c1c3be3a343acfdb3168f152d71fadc1" + integrity sha1-u8kQEcHDvjo0Os/bMWjxUtcfrcE= dependencies: - adaptive-expressions "4.10.0-preview-147186" + adaptive-expressions "4.10.0-preview-150886" antlr4ts "0.5.0-alpha.3" - lodash "^4.17.11" + lodash "^4.17.19" path "^0.12.7" - uuid "^3.3.3" + uuid "^3.4.0" boxen@^4.2.0: version "4.2.0" @@ -8464,8 +8469,8 @@ elegant-spinner@^1.0.1: elliptic@^6.0.0, elliptic@^6.5.3: version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -12400,8 +12405,8 @@ killable@^1.0.1: kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= kleur@^3.0.2: version "3.0.2" @@ -12825,8 +12830,8 @@ lodash.uniq@^4.5.0: lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha1-5I3e2+MLMyF4PFtDAfvTU7weSks= log-driver@^1.2.7: version "1.2.7" @@ -13346,8 +13351,8 @@ mixin-object@^2.0.1: mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= dependencies: minimist "^1.2.5" @@ -16846,8 +16851,8 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= dependencies: is-plain-object "^2.0.4" @@ -18649,11 +18654,6 @@ uuid@^3.0.0, uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"