diff --git a/Composer/packages/client/src/recoilModel/Recognizers.tsx b/Composer/packages/client/src/recoilModel/Recognizers.tsx index 051958dcb3..f0773813ec 100644 --- a/Composer/packages/client/src/recoilModel/Recognizers.tsx +++ b/Composer/packages/client/src/recoilModel/Recognizers.tsx @@ -144,6 +144,7 @@ export const Recognizer = React.memo((props: { projectId: string }) => { useEffect(() => { let recognizers: RecognizerFile[] = []; dialogs + .filter((dialog) => !dialog.isFormDialog) .filter((dialog) => isCrossTrainedRecognizerSet(dialog) || isLuisRecognizer(dialog)) .forEach((dialog) => { const filtedLus = luFiles.filter((item) => item.id.startsWith(dialog.id)); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index cfb2c95555..c4c7a9b646 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -45,6 +45,7 @@ const emptyDialog: DialogInfo = { triggers: [], intentTriggers: [], skills: [], + isFormDialog: false, }; type dialogStateParams = { projectId: string; dialogId: string }; export const dialogState = atomFamily({ diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 3e44f3ee05..82f5a9eba5 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -58,6 +58,7 @@ const stubDialog = (): DialogInfo => ({ triggers: [], intentTriggers: [], skills: [], + isFormDialog: false, }); export function useShell(source: EventSource, projectId: string): Shell { diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index 2e88ecfb5e..57d92b7bb8 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -180,6 +180,7 @@ function parse(id: string, content: any) { const luFile = typeof content.recognizer === 'string' ? content.recognizer : ''; const qnaFile = typeof content.recognizer === 'string' ? content.recognizer : ''; const lgFile = typeof content.generator === 'string' ? content.generator : ''; + const isFormDialog = has(content, 'schema'); // mark as form generated dialog; const diagnostics: Diagnostic[] = []; return { id, @@ -194,6 +195,7 @@ function parse(id: string, content: any) { triggers: extractTriggers(content), intentTriggers: extractIntentTriggers(content), skills: extractReferredSkills(content), + isFormDialog, }; } diff --git a/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts index 4a703382de..2f777111d3 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts @@ -9,105 +9,111 @@ const defaultLocale = 'en-us'; describe('Bot structure file path', () => { // cross-train config it('should get entry cross-train config file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'cross-train.config.json'); + const targetPath = defaultFilePath(botName, defaultLocale, 'cross-train.config.json', {}); expect(targetPath).toEqual('settings/cross-train.config.json'); }); // recognizer it('should get entry recognizer file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'test.lu.qna.dialog'); + const targetPath = defaultFilePath(botName, defaultLocale, 'test.lu.qna.dialog', {}); expect(targetPath).toEqual('dialogs/test/recognizers/test.lu.qna.dialog'); }); // entry dialog it('should get entry .dialog file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog'); + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog', {}); expect(targetPath).toEqual('mybot.dialog'); }); // common.lg it('should get common.lg file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'common.en-us.lg'); + const targetPath = defaultFilePath(botName, defaultLocale, 'common.en-us.lg', {}); expect(targetPath).toEqual('language-generation/en-us/common.en-us.lg'); }); // common.zh-cn.lg it('should get common..lg file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'common.zh-cn.lg'); + const targetPath = defaultFilePath(botName, defaultLocale, 'common.zh-cn.lg', {}); expect(targetPath).toEqual('language-generation/zh-cn/common.zh-cn.lg'); }); // skill manifest it('should get exported skill manifest file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'EchoBot-4-2-1-preview-1-manifest.json'); + const targetPath = defaultFilePath(botName, defaultLocale, 'EchoBot-4-2-1-preview-1-manifest.json', {}); expect(targetPath).toEqual('manifests/EchoBot-4-2-1-preview-1-manifest.json'); }); // mybot.en-us.lg it('should get entry dialog.lg file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lg'); + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lg', {}); expect(targetPath).toEqual('language-generation/en-us/mybot.en-us.lg'); }); // mybot.zh-cn.lg it('should get entry dialog.lg file path', async () => { - const targetPath = defaultFilePath('my-bot', defaultLocale, 'my-bot.zh-cn.lg'); + const targetPath = defaultFilePath('my-bot', defaultLocale, 'my-bot.zh-cn.lg', {}); expect(targetPath).toEqual('language-generation/zh-cn/my-bot.zh-cn.lg'); }); // entry dialog's lu it('should get entry dialog.lu file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lu'); + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lu', {}); expect(targetPath).toEqual('language-understanding/en-us/mybot.en-us.lu'); }); // child dialog's entry it('should get child dialog file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.dialog'); + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.dialog', {}); expect(targetPath).toEqual('dialogs/greeting/greeting.dialog'); }); // entry dialog's qna it('should get entry dialog.qna file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.qna'); + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.qna', {}); expect(targetPath).toEqual('knowledge-base/en-us/mybot.en-us.qna'); }); // entry dialog's source qna it('should get entry dialog.source.qna file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna'); + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna', {}); expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); }); // shared source qna it('should get shared .source.qna file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.source.qna'); + const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.source.qna', {}); expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); }); // child dialog's lg it('should get child dialog-lg file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lg'); + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lg', {}); expect(targetPath).toEqual('dialogs/greeting/language-generation/en-us/greeting.en-us.lg'); }); // child dialog's lu it('should get child dialog-lu file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lu'); + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lu', {}); expect(targetPath).toEqual('dialogs/greeting/language-understanding/en-us/greeting.en-us.lu'); }); // child dialog's qna it('should get child dialog-qna file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.qna'); + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.qna', {}); expect(targetPath).toEqual('dialogs/greeting/knowledge-base/en-us/greeting.en-us.qna'); }); // child dialog's source qna it('should get child dialog.source.qna file path', async () => { - const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna'); + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna', {}); expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/myimport1.source.qna'); }); + + // customized endpoint + it('should get child dialog.source.qna file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.qna', { endpoint: 'dialogs/Welcome' }); + expect(targetPath).toEqual('dialogs/Welcome/knowledge-base/en-us/myimport1.en-us.qna'); + }); }); describe('Parse file name', () => { diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 3090957ba4..cc220a995d 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -4,6 +4,7 @@ import { promisify } from 'util'; import fs from 'fs'; +import has from 'lodash/has'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; import { @@ -436,7 +437,14 @@ export class BotProject implements IBotProject { this._validateFileContent(name, content); const botName = this.name; const defaultLocale = this.settings?.defaultLanguage || defaultLanguage; - const relativePath = defaultFilePath(botName, defaultLocale, filename, this.rootDialogId); + + // find created file belong to which dialog, all resources should be writed to / + const dialogId = name.split('.')[0]; + const dialogFile = this.files.get(`${dialogId}.dialog`); + const endpoint = dialogFile ? Path.dirname(dialogFile.relativePath) : ''; + const rootDialogId = this.rootDialogId; + + const relativePath = defaultFilePath(botName, defaultLocale, filename, { endpoint, rootDialogId }); const file = this.files.get(filename); if (file) { throw new Error(`${filename} dialog already exist`); @@ -535,10 +543,10 @@ export class BotProject implements IBotProject { public async generateDialog(name: string, templateDirs?: string[]) { const defaultLocale = this.settings?.defaultLanguage || defaultLanguage; - const relativePath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.FormDialogSchema}`); + const relativePath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.FormDialogSchema}`, {}); const schemaPath = Path.resolve(this.dir, relativePath); - const dialogPath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.Dialog}`); + const dialogPath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.Dialog}`, {}); const outDir = Path.dirname(Path.resolve(this.dir, dialogPath)); const feedback = (type: FeedbackType, message: string): void => { @@ -585,7 +593,7 @@ export class BotProject implements IBotProject { public async deleteFormDialog(dialogId: string) { const defaultLocale = this.settings?.defaultLanguage || defaultLanguage; - const dialogPath = defaultFilePath(this.name, defaultLocale, `${dialogId}${FileExtensions.Dialog}`); + const dialogPath = defaultFilePath(this.name, defaultLocale, `${dialogId}${FileExtensions.Dialog}`, {}); const dirToDelete = Path.dirname(Path.resolve(this.dir, dialogPath)); // I check that the path is longer 3 to avoid deleting a drive and all its contents. @@ -790,11 +798,22 @@ export class BotProject implements IBotProject { // migration: create qna files for old bots private _createQnAFilesForOldBot = async (files: Map) => { + // flowing migration scripts depends on files; + this.files = new Map([...files]); const dialogFiles: FileInfo[] = []; const qnaFiles: FileInfo[] = []; files.forEach((file) => { if (file.name.endsWith('.dialog')) { - dialogFiles.push(file); + try { + // filter form dialog generated file. + const dialogJson = JSON.parse(file.content); + const isFormDialog = has(dialogJson, 'schema'); + if (!isFormDialog) { + dialogFiles.push(file); + } + } catch (_e) { + // ignore + } } if (file.name.endsWith('.qna')) { qnaFiles.push(file); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index c4dce265bf..6ce972eade 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -81,11 +81,15 @@ export const defaultFilePath = ( botName: string, defaultLocale: string, filename: string, - rootDialogId = '' + options: { + endpoint?: string; // / + rootDialogId?: string; + } ): string => { const BOTNAME = botName.toLowerCase(); const CommonFileId = 'common'; + const { endpoint = '', rootDialogId = '' } = options; const { fileId, locale, fileType, dialogId } = parseFileName(filename, defaultLocale); const LOCALE = locale; @@ -93,7 +97,11 @@ export const defaultFilePath = ( if (isRecognizer(filename)) { const dialogId = filename.split('.')[0]; const isRoot = filename.startsWith(botName) || (rootDialogId && filename.startsWith(rootDialogId)); - if (isRoot) { + if (endpoint) { + return templateInterpolate(Path.join(endpoint, BotStructureTemplate.recognizer), { + RECOGNIZERNAME: filename, + }); + } else if (isRoot) { return templateInterpolate(BotStructureTemplate.recognizer, { RECOGNIZERNAME: filename, }); @@ -137,19 +145,46 @@ export const defaultFilePath = ( const isRootFile = BOTNAME === DIALOGNAME.toLowerCase(); if (fileType === FileExtensions.SourceQnA) { + if (endpoint) { + return templateInterpolate(Path.join(endpoint, BotStructureTemplate.sourceQnA), { + FILENAME: fileId, + DIALOGNAME, + }); + } const TemplatePath = isRootFile || !dialogId ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; return templateInterpolate(TemplatePath, { FILENAME: fileId, DIALOGNAME, }); + } - return templateInterpolate(BotStructureTemplate.skillManifests, { - MANIFESTFILENAME: filename, + let TemplatePath = ''; + + if (endpoint) { + switch (fileType) { + case FileExtensions.Dialog: + TemplatePath = BotStructureTemplate.entry; + break; + case FileExtensions.Lg: + TemplatePath = BotStructureTemplate.lg; + break; + case FileExtensions.Lu: + TemplatePath = BotStructureTemplate.lu; + break; + case FileExtensions.Qna: + TemplatePath = BotStructureTemplate.qna; + break; + case FileExtensions.DialogSchema: + TemplatePath = BotStructureTemplate.dialogSchema; + } + return templateInterpolate(Path.join(endpoint, TemplatePath), { + BOTNAME: fileId, + DIALOGNAME, + LOCALE, }); } - let TemplatePath = ''; if (fileType === FileExtensions.Dialog) { TemplatePath = isRootFile ? BotStructureTemplate.entry : BotStructureTemplate.dialogs.entry; } else if (fileType === FileExtensions.Lg) { diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index a51401e16c..538d0d9bea 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -62,6 +62,7 @@ export type DialogInfo = { triggers: ITrigger[]; intentTriggers: IIntentTrigger[]; skills: string[]; + isFormDialog: boolean; }; export type LgTemplateJsonPath = {