diff --git a/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx b/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx index 01ee1470aa..0d1189ce62 100644 --- a/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx +++ b/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx @@ -15,6 +15,7 @@ import { schemasState, currentProjectIdState, botDiagnosticsState, + jsonSchemaFilesState, botProjectIdsState, formDialogSchemaIdsState, } from '../../../src/recoilModel'; @@ -80,6 +81,12 @@ const state = { ], }, ], + jsonSchemaFiles: [ + { + id: 'schema1.json', + content: 'test', + }, + ], diagnostics: [ { message: 'server error', @@ -105,6 +112,7 @@ const initRecoilState = ({ set }) => { set(dialogsState(state.projectId), state.dialogs); set(luFilesState(state.projectId), state.luFiles); set(lgFilesState(state.projectId), state.lgFiles); + set(jsonSchemaFilesState(state.projectId), state.jsonSchemaFiles); set(botDiagnosticsState(state.projectId), state.diagnostics); set(settingsState(state.projectId), state.settings); set(schemasState(state.projectId), mockProjectResponse.schemas); diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 18525e8195..2d07abd037 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -12,6 +12,7 @@ import { botProjectFileState, dialogSchemasState, formDialogSchemasSelectorFamily, + jsonSchemaFilesState, lgFilesState, luFilesState, qnaFilesState, @@ -43,6 +44,7 @@ export default function useNotifications(projectId: string, filter?: string) { const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const formDialogSchemas = useRecoilValue(formDialogSchemasSelectorFamily(projectId)); const botProjectFile = useRecoilValue(botProjectFileState(projectId)); + const jsonSchemaFiles = useRecoilValue(jsonSchemaFilesState(projectId)); const botAssets: BotAssets = { projectId, @@ -55,6 +57,7 @@ export default function useNotifications(projectId: string, filter?: string) { dialogSchemas, formDialogSchemas, botProjectFile, + jsonSchemaFiles, }; const memoized = useMemo(() => { diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 8e02c39105..c6f7852039 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -23,6 +23,7 @@ import { settingsState, filePersistenceState, botProjectFileState, + jsonSchemaFilesState, } from './atoms'; import { botsForFilePersistenceSelector, formDialogSchemasSelectorFamily } from './selectors'; @@ -37,6 +38,7 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = snapshot.getPromise(dialogSchemasState(projectId)), snapshot.getPromise(botProjectFileState(projectId)), snapshot.getPromise(formDialogSchemasSelectorFamily(projectId)), + snapshot.getPromise(jsonSchemaFilesState(projectId)), ]); return { projectId, @@ -49,6 +51,7 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = dialogSchemas: result[6], botProjectFile: result[7], formDialogSchemas: result[8], + jsonSchemaFiles: result[9], }; }; diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 43a906615b..0ecd54c188 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -10,6 +10,7 @@ import { DialogSchemaFile, DialogSetting, FormDialogSchema, + JsonSchemaFile, LgFile, LuFile, QnAFile, @@ -274,6 +275,11 @@ export const qnaFilesState = atomFamily({ default: [], }); +export const jsonSchemaFilesState = atomFamily({ + key: getFullyQualifiedKey('jsonSchemaFiles'), + default: [], +}); + export const filePersistenceState = atomFamily({ key: getFullyQualifiedKey('filePersistence'), default: {} as FilePersistence, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 81ce30ee06..e0bffa6dd2 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -49,6 +49,7 @@ import { filePersistenceState, formDialogSchemaIdsState, formDialogSchemaState, + jsonSchemaFilesState, lgFilesState, localeState, locationState, @@ -266,10 +267,11 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any const { dialogs, dialogSchemas, - formDialogSchemas, luFiles, lgFiles, qnaFiles, + jsonSchemaFiles, + formDialogSchemas, skillManifestFiles, skills, mergedSettings, @@ -309,6 +311,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(skillManifestsState(projectId), skillManifestFiles); set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); set(lgFilesState(projectId), lgFiles); + set(jsonSchemaFilesState(projectId), jsonSchemaFiles); set(dialogsState(projectId), verifiedDialogs); set(dialogSchemasState(projectId), dialogSchemas); set(botEnvironmentState(projectId), botEnvironment); diff --git a/Composer/packages/lib/indexers/__tests__/jsonSchemafileIndexer.test.ts b/Composer/packages/lib/indexers/__tests__/jsonSchemafileIndexer.test.ts new file mode 100644 index 0000000000..27ef6efd37 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/jsonSchemafileIndexer.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { jsonSchemaFileIndexer } from '../src/jsonSchemaFileIndexer'; +import { FileInfo } from '../src/type'; +import { getBaseName } from '../src/utils/help'; + +const { index } = jsonSchemaFileIndexer; + +describe('jsonSchemaFileIndexer', () => { + describe('index', () => { + it('should return json schema files', () => { + const input: FileInfo[] = [ + { + name: 'test1.json', + relativePath: './test1.json', + content: '{ "$schema":"http://json-schema.org/draft/schema" }', + path: '/', + }, + { + name: 'test2.json', + relativePath: './test2.json', + content: '{ "$schema":"http://json-schema.org/draft-07/schema" }', + path: '/', + }, + { + name: 'test3.json', + relativePath: './test3.json', + content: '{ "$schema":"http://json-schema.org/draft-07/schemav2" }', + path: '/', + }, + ]; + + const expected = input.map((item) => { + return { + name: getBaseName(item.name), + content: JSON.parse(item.content), + }; + }); + + const actual = index(input); + + expect(actual).toHaveLength(expected.length); + + for (let i = 0; i < actual.length; i++) { + expect(actual[i].id).toEqual(expected[i].name); + expect(actual[i].content.$schema).toEqual(expected[i].content.$schema); + } + }); + + it('should not return other json files', () => { + const input: FileInfo[] = [ + { + name: 'test1', + relativePath: './test1.json', + content: '{ "$schema":"http://microsoft.org/wsp" }', + path: '/', + }, + { + name: 'test2', + relativePath: './test2.json', + content: '{ "$schema":"http://json-schema.org/draft-07/schema" }', + path: '/', + }, + { + name: 'test3', + relativePath: './test3.json', + content: '{ "$schema":"http://microsoft.org/wsp" }', + path: '/', + }, + ]; + + const expected = [ + { + name: 'test2', + content: JSON.parse(input[1].content), + }, + ]; + + const actual = index(input); + + expect(actual).toHaveLength(expected.length); + + for (let i = 0; i < actual.length; i++) { + expect(actual[i].id).toEqual(expected[i].name); + expect(actual[i].content.$schema).toEqual(expected[i].content.$schema); + } + }); + }); +}); diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index d1663a9b60..de2a2ea730 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -118,6 +118,7 @@ function extractTriggers(dialog): ITrigger[] { displayName: '', type: rule.$kind, isIntent: rule.$kind === SDKKinds.OnIntent, + content: rule, }; if (has(rule, '$designer.name')) { trigger.displayName = rule.$designer.name; diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index dea46416ee..2a3356e2bb 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -4,6 +4,7 @@ import { DialogSetting, FileInfo, importResolverGenerator } from '@bfc/shared'; import { dialogIndexer } from './dialogIndexer'; import { dialogSchemaIndexer } from './dialogSchemaIndexer'; +import { jsonSchemaFileIndexer } from './jsonSchemaFileIndexer'; import { lgIndexer } from './lgIndexer'; import { luIndexer } from './luIndexer'; import { qnaIndexer } from './qnaIndexer'; @@ -60,6 +61,7 @@ class Indexer { skillManifestFiles: skillManifestIndexer.index(result[FileExtensions.Manifest]), skills: skillIndexer.index(skillContent, settings.skill), botProjectSpaceFiles: botProjectSpaceIndexer.index(result[FileExtensions.BotProjectSpace]), + jsonSchemaFiles: jsonSchemaFileIndexer.index(result[FileExtensions.Json]), formDialogSchemas: formDialogSchemaIndexer.index(result[FileExtensions.FormDialog]), }; } diff --git a/Composer/packages/lib/indexers/src/jsonSchemaFileIndexer.ts b/Composer/packages/lib/indexers/src/jsonSchemaFileIndexer.ts new file mode 100644 index 0000000000..94820aa29f --- /dev/null +++ b/Composer/packages/lib/indexers/src/jsonSchemaFileIndexer.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileInfo, JsonSchemaFile } from '@bfc/shared'; +import has from 'lodash/has'; + +import { getBaseName } from './utils/help'; + +const index = (jsonFiles: FileInfo[]) => { + return jsonFiles.reduce((jsonSchemaFiles: JsonSchemaFile[], { content, name }) => { + try { + const jsonContent = JSON.parse(content); + + if (has(jsonContent, '$schema')) { + const schema = jsonContent.$schema.toLowerCase().trim(); + if (schema.startsWith('http://json-schema.org')) { + return [...jsonSchemaFiles, { content: jsonContent, id: getBaseName(name) }]; + } + } + + return jsonSchemaFiles; + } catch (error) { + return jsonSchemaFiles; + } + }, [] as JsonSchemaFile[]); +}; + +export const jsonSchemaFileIndexer = { + index, +}; diff --git a/Composer/packages/lib/indexers/src/skillManifestIndexer.ts b/Composer/packages/lib/indexers/src/skillManifestIndexer.ts index fa0529db15..c833ed3553 100644 --- a/Composer/packages/lib/indexers/src/skillManifestIndexer.ts +++ b/Composer/packages/lib/indexers/src/skillManifestIndexer.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { FileInfo, SkillManifestInfo } from '@bfc/shared'; +import has from 'lodash/has'; import { getBaseName } from './utils/help'; @@ -9,7 +10,15 @@ const index = (skillManifestFiles: FileInfo[]) => { return skillManifestFiles.reduce((manifests: SkillManifestInfo[], { content, name, lastModified }) => { try { const jsonContent = JSON.parse(content); - return [...manifests, { content: jsonContent, id: getBaseName(name, '.json'), lastModified }]; + + if (has(jsonContent, '$schema')) { + const schema = jsonContent.$schema.toLowerCase().trim(); + if (schema.startsWith('https://schemas.botframework.com/schemas/skills')) { + return [...manifests, { content: jsonContent, id: getBaseName(name, '.json'), lastModified }]; + } + } + + return manifests; } catch (error) { return manifests; } diff --git a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts index e153e8f404..50301f905f 100644 --- a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts +++ b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts @@ -10,4 +10,5 @@ export enum FileExtensions { lg = '.lg', Manifest = '.json', BotProjectSpace = '.botproj', + Json = '.json', } diff --git a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts index 69ae7de469..ead0964b84 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -44,7 +44,7 @@ beforeEach(async () => { describe('init', () => { it('should get project successfully', () => { const project: { [key: string]: any } = proj.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); }); it('should always have a default bot project file', () => { @@ -123,7 +123,7 @@ describe('copyTo', () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); }); }); @@ -400,7 +400,7 @@ describe('deleteAllFiles', () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); await newBotProject.deleteAllFiles(); expect(fs.existsSync(copyDir)).toBe(false); }); diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 98faaab9ff..72f21cefd9 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -719,7 +719,7 @@ export class BotProject implements IBotProject { '**/*.lg', '**/*.lu', '**/*.qna', - 'manifests/*.json', + '**/*.json', 'sdk.override.schema', 'sdk.override.uischema', 'sdk.schema', diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index 77b264761b..b7db9085ab 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -17,6 +17,7 @@ export enum FileExtensions { Setting = 'appsettings.json', FormDialogSchema = '.form-dialog', BotProject = '.botproj', + Json = '.json', } export type FileInfo = { @@ -32,6 +33,7 @@ export type ITrigger = { displayName: string; type: string; isIntent: boolean; + content: any; }; export type ReferredLuIntents = { @@ -168,6 +170,11 @@ export type Skill = { name: string; }; +export type JsonSchemaFile = { + id: string; + content: string; +}; + export type TextFile = { id: string; content: string; @@ -200,6 +207,7 @@ export type BotAssets = { dialogSchemas: DialogSchemaFile[]; formDialogSchemas: FormDialogSchema[]; botProjectFile: BotProjectFile; + jsonSchemaFiles: JsonSchemaFile[]; }; export type BotInfo = {