@@ -612,7 +632,7 @@ const DesignPage: React.FC { - updateDialog({ id: currentDialog.id, content: data, projectId }); + updateDialog({ id: currentDialog.id, content: data, projectId: skillId ?? projectId }); }} /> ) : withWarning ? ( diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx index 43424dd313..63435f0e8d 100644 --- a/Composer/packages/client/src/pages/skills/skill-list.tsx +++ b/Composer/packages/client/src/pages/skills/skill-list.tsx @@ -131,7 +131,7 @@ const SkillList: React.FC = ({ projectId }) => { } }; - const handleEditSkill = (projectId, skillId) => (skillData) => { + const handleEditSkill = (projectId: string, skillId: string) => (skillData) => { updateSkill(projectId, skillId, skillData); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx index 8d24ddeb21..f6e49a076a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx @@ -127,7 +127,9 @@ describe('Bot Project File dispatcher', () => { dispatcher.addLocalSkillToBotProjectFile(testSkillId); }); - expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toBe('../Todo-Skill'); + expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toMatch( + /\.\.(\/|\\)Todo-Skill/ + ); expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.remote).toBeFalsy(); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx index f931e55c2f..9661ff113f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx @@ -21,7 +21,7 @@ import { } from '../../../utils/navigation'; import { createSelectedPath, getSelected } from '../../../utils/dialogUtil'; import { BreadcrumbItem } from '../../../recoilModel/types'; -import { currentProjectIdState } from '../../atoms'; +import { currentProjectIdState, botProjectIdsState, botProjectFileState, projectMetaDataState } from '../../atoms'; jest.mock('../../../utils/navigation'); jest.mock('../../../utils/dialogUtil'); @@ -35,6 +35,7 @@ const mockConvertPathToUrl = convertPathToUrl as jest.Mock; const mockCreateSelectedPath = createSelectedPath as jest.Mock; const projectId = '12345.678'; +const skillId = '98765.4321'; function expectNavTo(location: string, state: {} | null = null) { expect(mockNavigateTo).toHaveBeenLastCalledWith(location, state == null ? expect.anything() : state); @@ -80,11 +81,31 @@ describe('navigation dispatcher', () => { focused: 'b', }, }, + { + recoilState: designPageLocationState(skillId), + initialValue: { + dialogId: 'dialogInSkillId', + selected: 'a', + focused: 'b', + }, + }, { recoilState: currentProjectIdState, initialValue: projectId }, { recoilState: dialogsSelectorFamily(projectId), initialValue: [{ id: 'newDialogId', triggers: [{ type: SDKKinds.OnBeginDialog }] }], }, + { + recoilState: botProjectIdsState, + initialValue: [projectId], + }, + { + recoilState: botProjectFileState(projectId), + initialValue: { foo: 'bar' }, + }, + { + recoilState: projectMetaDataState(projectId), + initialValue: { isRootBot: true }, + }, ], dispatcher: { recoilState: dispatcherState, @@ -176,27 +197,16 @@ describe('navigation dispatcher', () => { it('navigates to a destination', async () => { mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/dialogId`); await act(async () => { - await dispatcher.navTo(projectId, null, 'dialogId', []); + await dispatcher.navTo(projectId, 'dialogId', []); }); expectNavTo(`/bot/${projectId}/dialogs/dialogId`); - expect(mockConvertPathToUrl).toBeCalledWith(projectId, null, 'dialogId', undefined); - }); - - it('redirects to the begin dialog trigger', async () => { - mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/newDialogId?selection=triggers[0]`); - mockCreateSelectedPath.mockReturnValue('triggers[0]'); - await act(async () => { - await dispatcher.navTo(projectId, null, 'newDialogId', []); - }); - expectNavTo(`/bot/${projectId}/dialogs/newDialogId?selection=triggers[0]`); - expect(mockConvertPathToUrl).toBeCalledWith(projectId, null, 'newDialogId', 'triggers[0]'); - expect(mockCreateSelectedPath).toBeCalledWith(0); + expect(mockConvertPathToUrl).toBeCalledWith(projectId, projectId, 'dialogId'); }); it("doesn't navigate to a destination where we already are", async () => { mockCheckUrl.mockReturnValue(true); await act(async () => { - await dispatcher.navTo(projectId, null, 'dialogId', []); + await dispatcher.navTo(projectId, 'dialogId', []); }); expect(mockNavigateTo).not.toBeCalled(); }); @@ -205,7 +215,7 @@ describe('navigation dispatcher', () => { describe('selectTo', () => { it("doesn't go anywhere without a selection", async () => { await act(async () => { - await dispatcher.selectTo(projectId, null, null, ''); + await dispatcher.selectTo(null, null, ''); }); expect(mockNavigateTo).not.toBeCalled(); }); @@ -213,16 +223,25 @@ describe('navigation dispatcher', () => { it('navigates to a default URL with selected path', async () => { mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/dialogId?selected=selection`); await act(async () => { - await dispatcher.selectTo(projectId, null, null, 'selection'); + await dispatcher.selectTo(null, null, 'selection'); }); expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=selection`); expect(mockConvertPathToUrl).toBeCalledWith(projectId, null, 'dialogId', 'selection'); }); + it('navigates to a default URL with skillId and selected path', async () => { + mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/skill/${skillId}/dialogs/dialogId?selected=selection`); + await act(async () => { + await dispatcher.selectTo(skillId, 'dialogId', 'selection'); + }); + expectNavTo(`/bot/${projectId}/skill/${skillId}/dialogs/dialogId?selected=selection`); + expect(mockConvertPathToUrl).toBeCalledWith(projectId, skillId, 'dialogId', 'selection'); + }); + it("doesn't go anywhere if we're already there", async () => { mockCheckUrl.mockReturnValue(true); await act(async () => { - await dispatcher.selectTo(projectId, null, null, 'selection'); + await dispatcher.selectTo(null, null, 'selection'); }); expect(mockNavigateTo).not.toBeCalled(); }); @@ -231,7 +250,7 @@ describe('navigation dispatcher', () => { describe('focusTo', () => { it('goes to the same page with no arguments', async () => { await act(async () => { - await dispatcher.focusTo(projectId, '', ''); + await dispatcher.focusTo(projectId, null, '', ''); }); expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=a`); }); @@ -239,28 +258,48 @@ describe('navigation dispatcher', () => { it('goes to a focused page', async () => { mockGetSelected.mockReturnValueOnce('select'); await act(async () => { - await dispatcher.focusTo(projectId, 'focus', ''); + await dispatcher.focusTo(projectId, null, 'focus', ''); }); expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=select&focused=focus`); expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected); expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused); }); + it('goes to a focused page with skill', async () => { + mockGetSelected.mockReturnValueOnce('select'); + await act(async () => { + await dispatcher.focusTo(projectId, skillId, 'focus', ''); + }); + expectNavTo(`/bot/${projectId}/skill/${skillId}/dialogs/dialogInSkillId?selected=select&focused=focus`); + expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected); + expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused); + }); + it('goes to a focused page with fragment', async () => { mockGetSelected.mockReturnValueOnce('select'); await act(async () => { - await dispatcher.focusTo(projectId, 'focus', 'fragment'); + await dispatcher.focusTo(projectId, null, 'focus', 'fragment'); }); expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=select&focused=focus#fragment`); expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected); expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused); }); + it('goes to a focused page with skill and fragment', async () => { + mockGetSelected.mockReturnValueOnce('select'); + await act(async () => { + await dispatcher.focusTo(projectId, skillId, 'focus', 'fragment'); + }); + expectNavTo(`/bot/${projectId}/skill/${skillId}/dialogs/dialogInSkillId?selected=select&focused=focus#fragment`); + expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected); + expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused); + }); + it('stays on the same page but updates breadcrumbs with a checked URL', async () => { mockCheckUrl.mockReturnValue(true); mockGetSelected.mockReturnValueOnce('select'); await act(async () => { - await dispatcher.focusTo(projectId, 'focus', 'fragment'); + await dispatcher.focusTo(projectId, null, 'focus', 'fragment'); }); expect(mockNavigateTo).not.toBeCalled(); expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected); @@ -269,7 +308,7 @@ describe('navigation dispatcher', () => { }); describe('selectAndFocus', () => { - it('sets selection and focus with a valud search', async () => { + it('sets selection and focus with a valid search', async () => { mockGetUrlSearch.mockReturnValue('?foo=bar&baz=quux'); await act(async () => { await dispatcher.selectAndFocus(projectId, null, 'dialogId', 'select', 'focus'); @@ -277,6 +316,14 @@ describe('navigation dispatcher', () => { expectNavTo(`/bot/${projectId}/dialogs/dialogId?foo=bar&baz=quux`); }); + it('sets selection and focus with a valid search and skillId', async () => { + mockGetUrlSearch.mockReturnValue('?foo=bar&baz=quux'); + await act(async () => { + await dispatcher.selectAndFocus(projectId, skillId, 'dialogId', 'select', 'focus'); + }); + expectNavTo(`/bot/${projectId}/skill/${skillId}/dialogs/dialogId?foo=bar&baz=quux`); + }); + it("doesn't go anywhere if we're already there", async () => { mockCheckUrl.mockReturnValue(true); await act(async () => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts b/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts index 23126d1946..e18df80e4d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts @@ -4,14 +4,13 @@ //TODO: refactor the router to use one-way data flow import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { PromptTab, SDKKinds } from '@bfc/shared'; -import cloneDeep from 'lodash/cloneDeep'; +import { PromptTab } from '@bfc/shared'; import { currentProjectIdState } from '../atoms'; import { encodeArrayPathToDesignerPath } from '../../utils/convertUtils/designerPathEncoder'; -import { dialogsSelectorFamily } from '../selectors'; +import { dialogsSelectorFamily, rootBotProjectIdSelector } from '../selectors'; -import { createSelectedPath, getSelected } from './../../utils/dialogUtil'; +import { getSelected } from './../../utils/dialogUtil'; import { BreadcrumbItem } from './../../recoilModel/types'; import { breadcrumbState, designPageLocationState, focusPathState } from './../atoms/botState'; import { @@ -50,44 +49,37 @@ export const navigationDispatcher = () => { const navTo = useRecoilCallback( ({ snapshot, set }: CallbackInterface) => async ( - projectId: string, skillId: string | null, - dialogId: string, + dialogId: string | null, breadcrumb: BreadcrumbItem[] = [] ) => { - const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); - const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId)); - const updatedBreadcrumb = cloneDeep(breadcrumb); - set(currentProjectIdState, projectId); - - let path; - if (dialogId !== designPageLocation.dialogId) { - const currentDialog = dialogs.find(({ id }) => id === dialogId); - const beginDialogIndex = currentDialog?.triggers.findIndex(({ type }) => type === SDKKinds.OnBeginDialog); + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (rootBotProjectId == null) return; - if (typeof beginDialogIndex !== 'undefined' && beginDialogIndex >= 0) { - path = createSelectedPath(beginDialogIndex); - path = encodeArrayPathToDesignerPath(currentDialog?.content, path); - updatedBreadcrumb.push({ dialogId, selected: '', focused: '' }); - } - } + const projectId = skillId ?? rootBotProjectId; - const currentUri = convertPathToUrl(projectId, skillId, dialogId, path); + const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId)); + set(currentProjectIdState, projectId); - if (checkUrl(currentUri, projectId, designPageLocation)) return; + const currentUri = convertPathToUrl(rootBotProjectId, projectId, dialogId); + if (checkUrl(currentUri, rootBotProjectId, projectId, designPageLocation)) return; - navigateTo(currentUri, { state: { breadcrumb: updatedBreadcrumb } }); + navigateTo(currentUri, { state: { breadcrumb } }); } ); const selectTo = useRecoilCallback( ({ snapshot, set }: CallbackInterface) => async ( - projectId: string, skillId: string | null, destinationDialogId: string | null, selectPath: string ) => { if (!selectPath) return; + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (rootBotProjectId == null) return; + + const projectId = skillId ?? rootBotProjectId; + set(currentProjectIdState, projectId); const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId)); const breadcrumb = await snapshot.getPromise(breadcrumbState(projectId)); @@ -98,25 +90,33 @@ export const navigationDispatcher = () => { const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); const currentDialog = dialogs.find(({ id }) => id === dialogId); const encodedSelectPath = encodeArrayPathToDesignerPath(currentDialog?.content, selectPath); - const currentUri = convertPathToUrl(projectId, skillId, dialogId, encodedSelectPath); + const currentUri = convertPathToUrl(rootBotProjectId, skillId, dialogId, encodedSelectPath); - if (checkUrl(currentUri, projectId, designPageLocation)) return; + if (checkUrl(currentUri, rootBotProjectId, skillId, designPageLocation)) return; navigateTo(currentUri, { state: { breadcrumb: updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected) } }); } ); const focusTo = useRecoilCallback( - ({ snapshot, set }: CallbackInterface) => async (projectId: string, focusPath: string, fragment: string) => { - set(currentProjectIdState, projectId); - const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId)); - const breadcrumb = await snapshot.getPromise(breadcrumbState(projectId)); + ({ snapshot, set }: CallbackInterface) => async ( + projectId: string, + skillId: string | null, + focusPath: string, + fragment: string + ) => { + set(currentProjectIdState, skillId ?? projectId); + const designPageLocation = await snapshot.getPromise(designPageLocationState(skillId ?? projectId)); + const breadcrumb = await snapshot.getPromise(breadcrumbState(skillId ?? projectId)); let updatedBreadcrumb = [...breadcrumb]; const { dialogId, selected } = designPageLocation; - let currentUri = `/bot/${projectId}/dialogs/${dialogId}`; + let currentUri = + skillId == null + ? `/bot/${projectId}/dialogs/${dialogId}` + : `/bot/${projectId}/skill/${skillId}/dialogs/${dialogId}`; if (focusPath) { - const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); + const dialogs = await snapshot.getPromise(dialogsSelectorFamily(skillId ?? projectId)); const currentDialog = dialogs.find(({ id }) => id === dialogId); const encodedFocusPath = encodeArrayPathToDesignerPath(currentDialog?.content, focusPath); @@ -135,7 +135,7 @@ export const navigationDispatcher = () => { if (fragment && typeof fragment === 'string') { currentUri += `#${fragment}`; } - if (checkUrl(currentUri, projectId, designPageLocation)) return; + if (checkUrl(currentUri, projectId, skillId, designPageLocation)) return; navigateTo(currentUri, { state: { breadcrumb: updatedBreadcrumb } }); } ); @@ -158,12 +158,15 @@ export const navigationDispatcher = () => { const search = getUrlSearch(encodedSelectPath, encodedFocusPath); const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId)); if (search) { - const currentUri = `/bot/${projectId}/dialogs/${dialogId}${search}`; + const currentUri = + skillId == null + ? `/bot/${projectId}/dialogs/${dialogId}${search}` + : `/bot/${projectId}/skill/${skillId}/dialogs/${dialogId}${search}`; - if (checkUrl(currentUri, projectId, designPageLocation)) return; + if (checkUrl(currentUri, projectId, skillId, designPageLocation)) return; navigateTo(currentUri, { state: { breadcrumb } }); } else { - navTo(projectId, skillId, dialogId, breadcrumb); + navTo(skillId ?? projectId, dialogId, breadcrumb); } } ); diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts index 8f59646139..453eca41b0 100644 --- a/Composer/packages/client/src/recoilModel/types.ts +++ b/Composer/packages/client/src/recoilModel/types.ts @@ -77,6 +77,7 @@ export interface AppUpdateState { } export interface BreadcrumbItem { + skillId?: string; dialogId: string; selected: string; focused: string; diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index 037bc9eff8..fda81b41a4 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -70,6 +70,13 @@ const Routes = (props) => { /> ))} + + + + + + + @@ -97,7 +104,7 @@ const projectStyle = css` label: ProjectRouter; `; -const ProjectRouter: React.FC> = (props) => { +const ProjectRouter: React.FC> = (props) => { const { projectId = '' } = props; const schemas = useRecoilValue(schemasState(projectId)); const { fetchProjectById } = useRecoilValue(dispatcherState); @@ -119,7 +126,11 @@ const ProjectRouter: React.FC> = (pro }, [schemas, projectId]); if (props.projectId && botProjects.includes(props.projectId)) { - return
{props.children}
; + if (props.skillId && !botProjects.includes(props.skillId)) { + return ; + } else { + return
{props.children}
; + } } return ; }; diff --git a/Composer/packages/client/src/shell/triggerApi.ts b/Composer/packages/client/src/shell/triggerApi.ts index 44c51ce5d2..c1d6ceb6c5 100644 --- a/Composer/packages/client/src/shell/triggerApi.ts +++ b/Composer/packages/client/src/shell/triggerApi.ts @@ -112,7 +112,7 @@ function createTriggerApi( }; await updateDialog(dialogPayload); if (autoSelected) { - selectTo(projectId, null, null, `triggers[${index}]`); + selectTo(projectId, newDialog.id, `triggers[${index}]`); } }; diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 13c6d19a00..3e44f3ee05 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -27,6 +27,7 @@ import { lgFilesState, luFilesState, rateInfoState, + rootBotProjectIdSelector, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; @@ -78,6 +79,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const botName = useRecoilValue(botDisplayNameState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const flowZoomRate = useRecoilValue(rateInfoState); + const rootBotProjectId = useRecoilValue(rootBotProjectIdSelector); const userSettings = useRecoilValue(userSettingsState); const clipboardActions = useRecoilValue(clipboardActionsState); @@ -135,11 +137,13 @@ export function useShell(source: EventSource, projectId: string): Shell { } function navigationTo(path) { - navTo(projectId, null, path, breadcrumb); + if (rootBotProjectId == null) return; + navTo(projectId, path, breadcrumb); } function focusEvent(subPath) { - selectTo(projectId, null, null, subPath); + if (rootBotProjectId == null) return; + selectTo(projectId, dialogId, subPath); } function focusSteps(subPaths: string[] = [], fragment?: string) { @@ -154,7 +158,7 @@ export function useShell(source: EventSource, projectId: string): Shell { } } - focusTo(projectId, dataPath, fragment ?? ''); + focusTo(rootBotProjectId ?? projectId, projectId, dataPath, fragment ?? ''); } function updateFlowZoomRate(currentRate) { diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index 773e5b0c6b..dcc7b19fe4 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -69,9 +69,13 @@ export function getUrlSearch(selected: string, focused: string): string { export function checkUrl( currentUri: string, projectId: string, + skillId: string | null, { dialogId, selected, focused, promptTab }: DesignPageLocation ) { - let lastUri = `/bot/${projectId}/dialogs/${dialogId}${getUrlSearch(selected, focused)}`; + let lastUri = + skillId == null + ? `/bot/${projectId}/dialogs/${dialogId}${getUrlSearch(selected, focused)}` + : `/bot/${projectId}/skill/${skillId}/dialogs/${dialogId}${getUrlSearch(selected, focused)}`; if (promptTab) { lastUri += `#${promptTab}`; } @@ -83,14 +87,23 @@ export interface NavigationState { qnaKbUrls?: string[]; } -export function convertPathToUrl(projectId: string, skillId: string | null, dialogId: string, path?: string): string { +export function convertPathToUrl( + projectId: string, + skillId: string | null, + dialogId: string | null, + path?: string +): string { //path is like main.triggers[0].actions[0] //uri = id?selected=triggers[0]&focused=triggers[0].actions[0] - let uri = - skillId == null - ? `/bot/${projectId}/dialogs/${dialogId}` - : `/bot/${projectId}/skill/${skillId}/dialogs/${dialogId}`; + let uri = `/bot/${projectId}`; + if (skillId != null) { + uri += `/skill/${skillId}`; + } + if (dialogId != null) { + uri += `/dialogs/${dialogId}`; + } + if (!path) return uri; const items = path.split('#'); diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/recognizers/cross-train.config.json b/Composer/packages/server/src/__mocks__/samplebots/bot1/recognizers/cross-train.config.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/bot1/recognizers/cross-train.config.json @@ -0,0 +1 @@ +{} \ No newline at end of file