diff --git a/Composer/packages/client/__tests__/components/skill.test.tsx b/Composer/packages/client/__tests__/components/skill.test.tsx index 1fe175c270..67b0bdf4b1 100644 --- a/Composer/packages/client/__tests__/components/skill.test.tsx +++ b/Composer/packages/client/__tests__/components/skill.test.tsx @@ -22,24 +22,24 @@ jest.mock('../../src/components/Modal/dialogStyle', () => ({})); const skills: Skill[] = [ { + id: 'email-skill', + content: {}, manifestUrl: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json', name: 'Email-Skill', description: 'The Email skill provides email related capabilities and supports Office and Google calendars.', endpointUrl: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/api/messages', msAppId: '79432da8-0f7e-4a16-8c23-ddbba30ae85d', - protocol: '', endpoints: [], - body: '', }, { + id: 'point-of-interest-skill', + content: {}, manifestUrl: 'https://hualxielearn2-snskill.azurewebsites.net/manifest/manifest-1.0.json', name: 'Point Of Interest Skill', description: 'The Point of Interest skill provides PoI search capabilities leveraging Azure Maps and Foursquare.', endpointUrl: 'https://hualxielearn2-snskill.azurewebsites.net/api/messages', msAppId: 'e2852590-ea71-4a69-9e44-e74b5b6cbe89', - protocol: '', endpoints: [], - body: '', }, ]; @@ -101,7 +101,7 @@ describe('', () => { it('should render the skill form, and update skill manifest url', () => { jest.useFakeTimers(); - (httpClient.post as jest.Mock).mockResolvedValue({ endpoints: [] }); + (httpClient.get as jest.Mock).mockResolvedValue({ endpoints: [] }); const onDismiss = jest.fn(); const onSubmit = jest.fn(); @@ -203,7 +203,7 @@ describe('', () => { }); it('should try and retrieve manifest if manifest url meets other criteria', async () => { - (httpClient.post as jest.Mock) = jest.fn().mockResolvedValue({ data: 'skill manifest' }); + (httpClient.get as jest.Mock) = jest.fn().mockResolvedValue({ data: 'skill manifest' }); const formData = { manifestUrl: 'https://skill' }; const formDataErrors = { manifestUrl: 'error' }; @@ -223,7 +223,11 @@ describe('', () => { manifestUrl: 'Validating', }) ); - expect(httpClient.post).toBeCalledWith(`/projects/${projectId}/skill/check`, { url: formData.manifestUrl }); + expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + params: { + url: formData.manifestUrl, + }, + }); expect(setSkillManifest).toBeCalledWith('skill manifest'); expect(setValidationState).toBeCalledWith( expect.objectContaining({ @@ -238,7 +242,7 @@ describe('', () => { }); it('should show error when it could not retrieve skill manifest', async () => { - (httpClient.post as jest.Mock) = jest.fn().mockRejectedValue({ message: 'skill manifest' }); + (httpClient.get as jest.Mock) = jest.fn().mockRejectedValue({ message: 'skill manifest' }); const formData = { manifestUrl: 'https://skill' }; @@ -257,7 +261,11 @@ describe('', () => { manifestUrl: 'Validating', }) ); - expect(httpClient.post).toBeCalledWith(`/projects/${projectId}/skill/check`, { url: formData.manifestUrl }); + expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + params: { + url: formData.manifestUrl, + }, + }); expect(setSkillManifest).not.toBeCalled(); expect(setValidationState).toBeCalledWith( expect.objectContaining({ diff --git a/Composer/packages/client/src/components/CreateSkillModal.tsx b/Composer/packages/client/src/components/CreateSkillModal.tsx index f52408d152..f42a21b491 100644 --- a/Composer/packages/client/src/components/CreateSkillModal.tsx +++ b/Composer/packages/client/src/components/CreateSkillModal.tsx @@ -11,7 +11,7 @@ import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { useRecoilValue } from 'recoil'; import debounce from 'lodash/debounce'; -import { Skill } from '@bfc/shared'; +import { SkillSetting } from '@bfc/shared'; import { addSkillDialog } from '../constants'; import httpClient from '../utils/httpUtil'; @@ -31,7 +31,7 @@ export const msAppIdRegex = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A export interface CreateSkillModalProps { projectId: string; - onSubmit: (data: Skill) => void; + onSubmit: (data: SkillSetting) => void; onDismiss: () => void; } @@ -83,7 +83,11 @@ export const validateManifestUrl = async ({ } else { try { setValidationState({ ...validationState, manifestUrl: ValidationState.Validating }); - const { data } = await httpClient.post(`/projects/${projectId}/skill/check`, { url: manifestUrl }); + const { data } = await httpClient.get(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + params: { + url: manifestUrl, + }, + }); setFormDataErrors(errors); setSkillManifest(data); setValidationState({ ...validationState, manifestUrl: ValidationState.Validated }); @@ -118,7 +122,7 @@ export const validateName = ({ export const CreateSkillModal: React.FC = ({ projectId, onSubmit, onDismiss }) => { const skills = useRecoilValue(skillsState(projectId)); - const [formData, setFormData] = useState>({}); + const [formData, setFormData] = useState>({}); const [formDataErrors, setFormDataErrors] = useState({}); const [validationState, setValidationState] = useState({ endpoint: ValidationState.NotValidated, @@ -208,7 +212,7 @@ export const CreateSkillModal: React.FC = ({ projectId, o Object.values(validationState).every((validation) => validation === ValidationState.Validated) && !Object.values(formDataErrors).some(Boolean) ) { - onSubmit(formData as Skill); + onSubmit({ name: skillManifest.name, ...formData } as SkillSetting); } }; diff --git a/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx b/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx index 0f29520e55..70ba9257ba 100644 --- a/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx +++ b/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx @@ -98,7 +98,7 @@ export const DisplayManifestModal: React.FC = ({ height={'100%'} id={'modaljsonview'} options={{ readOnly: true }} - value={JSON.parse(selectedSkill.body ?? '')} + value={selectedSkill.content} onChange={() => {}} /> diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx index 901c0ac06e..bdfa8467b9 100644 --- a/Composer/packages/client/src/pages/skills/index.tsx +++ b/Composer/packages/client/src/pages/skills/index.tsx @@ -7,7 +7,7 @@ import { RouteComponentProps } from '@reach/router'; import React, { useCallback, useState } from 'react'; import formatMessage from 'format-message'; import { useRecoilValue } from 'recoil'; -import { Skill } from '@bfc/shared'; +import { SkillSetting } from '@bfc/shared'; import { dispatcherState, settingsState, botNameState } from '../../recoilModel'; import { Toolbar, IToolbarItem } from '../../components/Toolbar'; @@ -48,7 +48,7 @@ const Skills: React.FC> = (props) => ]; const onSubmitForm = useCallback( - (skill: Skill) => { + (skill: SkillSetting) => { addSkill(projectId, skill); setShowAddSkillDialogModal(false); }, diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx index 7d19e28522..43424dd313 100644 --- a/Composer/packages/client/src/pages/skills/skill-list.tsx +++ b/Composer/packages/client/src/pages/skills/skill-list.tsx @@ -126,22 +126,22 @@ const SkillList: React.FC = ({ projectId }) => { const [selectedSkillUrl, setSelectedSkillUrl] = useState(null); const handleViewManifest = (item) => { - if (item && item.name && item.body) { + if (item && item.name && item.content) { setSelectedSkillUrl(item.manifestUrl); } }; - const handleEditSkill = (targetId) => (skillData) => { - updateSkill(projectId, { skillData, targetId }); + const handleEditSkill = (projectId, skillId) => (skillData) => { + updateSkill(projectId, skillId, skillData); }; const items = useMemo( () => - skills.map((skill, index) => ({ + skills.map((skill) => ({ skill, - onDelete: () => removeSkill(projectId, skill.manifestUrl), + onDelete: () => removeSkill(projectId, skill.id), onViewManifest: () => handleViewManifest(skill), - onEditSkill: handleEditSkill(index), + onEditSkill: handleEditSkill(projectId, skill.id), })), [skills, projectId] ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx index c2f11a5151..d5676354c2 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx @@ -5,10 +5,13 @@ import { useRecoilValue } from 'recoil'; import { act } from '@bfc/test-utils/lib/hooks'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; -import { settingsState, currentProjectIdState } from '../../atoms'; +import { settingsState, currentProjectIdState, skillsState } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '..'; import { settingsDispatcher } from '../setting'; +import httpClient from '../../../utils/httpUtil'; + +jest.mock('../../../utils/httpUtil'); const projectId = '1235a.2341'; @@ -65,6 +68,7 @@ const settings = { maxImbalanceRatio: 10, maxUtteranceAllowed: 15000, }, + skill: {}, }; describe('setting dispatcher', () => { @@ -72,10 +76,11 @@ describe('setting dispatcher', () => { beforeEach(() => { const useRecoilTestHook = () => { const settings = useRecoilValue(settingsState(projectId)); + const skills = useRecoilValue(skillsState(projectId)); const currentDispatcher = useRecoilValue(dispatcherState); - return { settings, + skills, currentDispatcher, }; }; @@ -84,6 +89,7 @@ describe('setting dispatcher', () => { states: [ { recoilState: settingsState(projectId), initialValue: settings }, { recoilState: currentProjectIdState, initialValue: projectId }, + { recoilState: skillsState(projectId), initialValue: [] }, ], dispatcher: { recoilState: dispatcherState, @@ -151,4 +157,38 @@ describe('setting dispatcher', () => { }); expect(renderedComponent.current.settings.runtime.customRuntime).toBeTruthy(); }); + + it('should update skills state', async () => { + (httpClient.get as jest.Mock).mockResolvedValue({ + data: { description: 'description', endpoints: [{ endpointUrl: 'https://test' }] }, + }); + + await act(async () => { + await dispatcher.setSettings(projectId, { + skill: { + foo: { + msAppId: '00000000-0000', + endpointUrl: 'https://skill-manifest/api/messages', + name: 'foo', + manifestUrl: 'https://skill-manifest', + }, + }, + } as any); + }); + + expect(renderedComponent.current.skills).toEqual( + expect.arrayContaining([ + { + id: 'foo', + name: 'foo', + manifestUrl: 'https://skill-manifest', + msAppId: '00000000-0000', + endpointUrl: 'https://skill-manifest/api/messages', + description: 'description', + endpoints: expect.any(Array), + content: expect.any(Object), + }, + ]) + ); + }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts index 39d6078de4..61ebcd3c70 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts @@ -35,14 +35,17 @@ const mockDialogComplete = jest.fn(); const projectId = '42345.23432'; const makeTestSkill: (number) => Skill = (n) => ({ + id: 'id' + n, manifestUrl: 'url' + n, name: 'skill' + n, - protocol: 'GET', description: 'test skill' + n, endpointUrl: 'url', endpoints: [{ test: 'foo' }], msAppId: 'ID', - body: 'body', + content: { + description: 'test skill' + n, + endpoints: [{ test: 'foo' }], + }, }); describe('skill dispatcher', () => { @@ -143,19 +146,30 @@ describe('skill dispatcher', () => { it('updateSkill', async () => { await act(async () => { - dispatcher.updateSkill(projectId, { - targetId: 0, - skillData: makeTestSkill(100), + dispatcher.updateSkill(projectId, 'id1', { + msAppId: 'test', + manifestUrl: 'test', + endpointUrl: 'test', + name: 'test', }); }); - expect(renderedComponent.current.skills).not.toContain(makeTestSkill(1)); - expect(renderedComponent.current.skills).toContainEqual(makeTestSkill(100)); + expect(renderedComponent.current.skills[0]).toEqual( + expect.objectContaining({ + id: 'id1', + content: {}, + name: 'test', + msAppId: 'test', + manifestUrl: 'test', + endpointUrl: 'test', + endpoints: [], + }) + ); }); it('removeSkill', async () => { await act(async () => { - dispatcher.removeSkill(projectId, makeTestSkill(1).manifestUrl); + dispatcher.removeSkill(projectId, makeTestSkill(1).id); }); expect(renderedComponent.current.skills).not.toContain(makeTestSkill(1)); }); @@ -176,32 +190,6 @@ describe('skill dispatcher', () => { expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(undefined); }); - describe('addSkillDialogSuccess', () => { - it('with a function in onAddSkillDialogComplete', async () => { - await act(async () => { - dispatcher.addSkillDialogBegin(mockDialogComplete, projectId); - }); - await act(async () => { - dispatcher.addSkillDialogSuccess(projectId); - }); - expect(mockDialogComplete).toHaveBeenCalledWith(null); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(false); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(undefined); - }); - - it('with nothing in onAddSkillDialogComplete', async () => { - await act(async () => { - dispatcher.addSkillDialogCancel(projectId); - }); - await act(async () => { - dispatcher.addSkillDialogSuccess(projectId); - }); - expect(mockDialogComplete).not.toHaveBeenCalled(); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(false); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(undefined); - }); - }); - it('displayManifestModal', async () => { await act(async () => { dispatcher.displayManifestModal('foo', projectId); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index c5136d015a..506b2a5ff9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -143,6 +143,7 @@ const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: Dia ); return updateQnaFilesStatus(projectId, qnaFiles); }; + export const projectDispatcher = () => { const initBotState = async ( callbackHelpers: CallbackInterface, @@ -152,7 +153,17 @@ export const projectDispatcher = () => { qnaKbUrls?: string[] ) => { const { snapshot, gotoSnapshot, set } = callbackHelpers; - const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data; + const { + files, + botName, + botEnvironment, + location, + schemas, + settings, + id: projectId, + diagnostics, + skills: skillContent, + } = data; const curLocation = await snapshot.getPromise(locationState(projectId)); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; @@ -160,6 +171,12 @@ export const projectDispatcher = () => { // cache current projectId in session, resolve page refresh caused state lost. projectIdCache.set(projectId); + const mergedSettings = mergeLocalStorage(projectId, settings); + if (Array.isArray(mergedSettings.skill)) { + const skillsArr = mergedSettings.skill.map((skillData) => ({ ...skillData })); + mergedSettings.skill = convertSkillsToDictionary(skillsArr); + } + try { schemas.sdk.content = processSchema(projectId, schemas.sdk.content); } catch (err) { @@ -169,10 +186,12 @@ export const projectDispatcher = () => { } try { - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles } = indexer.index( + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills } = indexer.index( files, botName, - locale + locale, + skillContent, + mergedSettings ); let mainDialog = ''; @@ -207,15 +226,6 @@ export const projectDispatcher = () => { set(botDiagnosticsState(projectId), diagnostics); refreshLocalStorage(projectId, settings); - const mergedSettings = mergeLocalStorage(projectId, settings); - if (Array.isArray(mergedSettings.skill)) { - const skillsArr = mergedSettings.skill.map((skillData) => { - return { - ...skillData, - }; - }); - mergedSettings.skill = convertSkillsToDictionary(skillsArr); - } set(settingsState(projectId), mergedSettings); set(filePersistenceState(projectId), new FilePersistence(projectId)); set(undoHistoryState(projectId), new UndoHistory(projectId)); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts index b8fc5f344c..d13761e46b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts @@ -4,26 +4,66 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { SensitiveProperties, DialogSetting, PublishTarget, Skill } from '@bfc/shared'; +import { skillIndexer } from '@bfc/indexers'; import get from 'lodash/get'; import has from 'lodash/has'; +import isEqual from 'lodash/isEqual'; +import keys from 'lodash/keys'; import settingStorage from '../../utils/dialogSettingStorage'; -import { settingsState } from '../atoms/botState'; +import { settingsState, skillsState } from '../atoms/botState'; import httpClient from './../../utils/httpUtil'; import { setError } from './shared'; +export const setSettingState = async ( + callbackHelpers: CallbackInterface, + projectId: string, + settings: DialogSetting +) => { + const { set, snapshot } = callbackHelpers; + const previousSettings = await snapshot.getPromise(settingsState(projectId)); + + if (!isEqual(settings.skill, previousSettings.skill)) { + const skills = await snapshot.getPromise(skillsState(projectId)); + const skillContent = await Promise.all( + keys(settings.skill).map(async (id) => { + if (settings?.skill?.[id]?.manifestUrl !== previousSettings?.skill?.[id]?.manifestUrl) { + try { + const { data: content } = await httpClient.get(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + params: { + url: settings?.skill?.[id]?.manifestUrl, + }, + }); + return { id, content }; + } catch (error) { + return { id }; + } + } + + const { content = {} } = skills.find(({ id: key }) => id === key) || ({} as Skill); + + return { id, content }; + }) + ); + + set(skillsState(projectId), skillIndexer.index(skillContent, settings.skill)); + } + + // set value in local storage + for (const property of SensitiveProperties) { + if (has(settings, property)) { + const propertyValue = get(settings, property, ''); + settingStorage.setField(projectId, property, propertyValue); + } + } + set(settingsState(projectId), settings); +}; + export const settingsDispatcher = () => { const setSettings = useRecoilCallback<[string, DialogSetting], Promise>( - ({ set }: CallbackInterface) => async (projectId: string, settings: DialogSetting) => { - // set value in local storage - for (const property of SensitiveProperties) { - if (has(settings, property)) { - const propertyValue = get(settings, property, ''); - settingStorage.setField(projectId, property, propertyValue); - } - } - set(settingsState(projectId), settings); + (callbackHelpers: CallbackInterface) => async (projectId: string, settings: DialogSetting) => { + setSettingState(callbackHelpers, projectId, settings); } ); @@ -89,29 +129,6 @@ export const settingsDispatcher = () => { } ); - const updateSkillsInSetting = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ( - skillName: string, - skillInfo: Partial, - projectId: string - ) => { - const currentSettings: DialogSetting = await snapshot.getPromise(settingsState(projectId)); - const matchedSkill = get(currentSettings, `skill[${skillName}]`, undefined); - if (matchedSkill) { - set(settingsState(projectId), { - ...currentSettings, - skill: { - ...currentSettings.skill, - [skillName]: { - ...matchedSkill, - ...skillInfo, - }, - }, - }); - } - } - ); - return { setSettings, setRuntimeSettings, @@ -119,6 +136,5 @@ export const settingsDispatcher = () => { setRuntimeField, setCustomRuntime, setQnASettings, - updateSkillsInSetting, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts index d728fa46a6..c7938aacbd 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts @@ -3,19 +3,17 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { CallbackInterface, useRecoilCallback } from 'recoil'; -import { SkillManifest, convertSkillsToDictionary, Skill } from '@bfc/shared'; - -import httpClient from '../../utils/httpUtil'; +import { SkillManifest, SkillSetting } from '@bfc/shared'; +import produce from 'immer'; import { skillManifestsState, onAddSkillDialogCompleteState, - skillsState, - settingsState, showAddSkillDialogModalState, displaySkillManifestState, + settingsState, } from './../atoms/botState'; -import { logMessage } from './shared'; +import { setSettingState } from './setting'; export const skillDispatcher = () => { const createSkillManifest = ({ set }, { id, content, projectId }) => { @@ -41,38 +39,19 @@ export const skillDispatcher = () => { } ); - const updateSkillState = async ( - callbackHelpers: CallbackInterface, - projectId: string, - updatedSkills: Skill[] - ): Promise => { - try { - const { set } = callbackHelpers; - - const { data: skills } = await httpClient.post(`/projects/${projectId}/skills/`, { skills: updatedSkills }); - - set(settingsState(projectId), (settings) => ({ - ...settings, - skill: convertSkillsToDictionary(skills), - })); - set(skillsState(projectId), skills); - - return skills; - } catch (error) { - logMessage(callbackHelpers, error.message); - } - }; - const addSkill = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string, skillData: Skill) => { + (callbackHelpers: CallbackInterface) => async (projectId: string, skill: SkillSetting) => { const { set, snapshot } = callbackHelpers; const { func: onAddSkillDialogComplete } = await snapshot.getPromise(onAddSkillDialogCompleteState(projectId)); - const skills = await updateSkillState(callbackHelpers, projectId, [ - ...(await snapshot.getPromise(skillsState(projectId))), - skillData, - ]); - - const skill = (skills || []).find(({ manifestUrl }) => manifestUrl === skillData.manifestUrl); + const settings = await snapshot.getPromise(settingsState(projectId)); + + setSettingState( + callbackHelpers, + projectId, + produce(settings, (updateSettings) => { + updateSettings.skill = { ...(updateSettings.skill || {}), [skill.name]: skill }; + }) + ); if (typeof onAddSkillDialogComplete === 'function') { onAddSkillDialogComplete(skill || null); @@ -84,29 +63,44 @@ export const skillDispatcher = () => { ); const removeSkill = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string, manifestUrl?: string) => { + (callbackHelpers: CallbackInterface) => async (projectId: string, key: string) => { const { snapshot } = callbackHelpers; - const currentSkills = await snapshot.getPromise(skillsState(projectId)); - const skills = currentSkills.filter((skill) => skill.manifestUrl !== manifestUrl); - await updateSkillState(callbackHelpers, projectId, skills); + const settings = await snapshot.getPromise(settingsState(projectId)); + + setSettingState( + callbackHelpers, + projectId, + produce(settings, (updateSettings) => { + delete updateSettings.skill?.[key]; + }) + ); } ); const updateSkill = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ( projectId: string, - { targetId, skillData }: { targetId: number; skillData?: any } + key: string, + { endpointUrl, manifestUrl, msAppId, name }: SkillSetting ) => { const { snapshot } = callbackHelpers; - const originSkills = [...(await snapshot.getPromise(skillsState(projectId)))]; - - if (targetId >= 0 && targetId < originSkills.length && skillData) { - originSkills.splice(targetId, 1, skillData); - } else { - throw new Error(`update out of range, skill not found`); - } - - updateSkillState(callbackHelpers, projectId, originSkills); + const settings = await snapshot.getPromise(settingsState(projectId)); + + setSettingState( + callbackHelpers, + projectId, + produce(settings, (updateSettings) => { + updateSettings.skill = { + ...(updateSettings.skill || {}), + [key]: { + endpointUrl, + manifestUrl, + msAppId, + name, + }, + }; + }) + ); } ); @@ -117,21 +111,9 @@ export const skillDispatcher = () => { const addSkillDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => (projectId: string) => { set(showAddSkillDialogModalState(projectId), false); - set(onAddSkillDialogCompleteState(projectId), { func: undefined }); + set(onAddSkillDialogCompleteState(projectId), {}); }); - const addSkillDialogSuccess = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { - const onAddSkillDialogComplete = (await snapshot.getPromise(onAddSkillDialogCompleteState(projectId))).func; - if (typeof onAddSkillDialogComplete === 'function') { - onAddSkillDialogComplete(null); - } - - set(showAddSkillDialogModalState(projectId), false); - set(onAddSkillDialogCompleteState(projectId), { func: undefined }); - } - ); - const displayManifestModal = useRecoilCallback(({ set }: CallbackInterface) => (id: string, projectId: string) => { set(displaySkillManifestState(projectId), id); }); @@ -142,15 +124,14 @@ export const skillDispatcher = () => { return { addSkill, + removeSkill, + updateSkill, createSkillManifest, removeSkillManifest, updateSkillManifest, - updateSkill, addSkillDialogBegin, addSkillDialogCancel, - addSkillDialogSuccess, displayManifestModal, dismissManifestModal, - removeSkill, }; }; diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index f80bacaafc..de01dcbee3 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { useMemo, useRef } from 'react'; -import { ShellApi, ShellData, Shell, fetchFromSettings, DialogSchemaFile, Skill } from '@bfc/shared'; +import { ShellApi, ShellData, Shell, fetchFromSettings, DialogSchemaFile, SkillSetting } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; @@ -73,7 +73,7 @@ export function useShell(source: EventSource, projectId: string): Shell { updateUserSettings, setMessage, displayManifestModal, - updateSkillsInSetting, + updateSkill, } = useRecoilValue(dispatcherState); const lgApi = useLgApi(projectId); @@ -197,15 +197,15 @@ export function useShell(source: EventSource, projectId: string): Shell { redo, commitChanges, addCoachMarkRef: onboardingAddCoachMarkRef, - updateUserSettings: updateUserSettings, + updateUserSettings, announce: setMessage, displayManifestModal: (skillId) => displayManifestModal(skillId, projectId), updateDialogSchema: async (dialogSchema: DialogSchemaFile) => { updateDialogSchema(dialogSchema, projectId); }, - skillsInSettings: { + skillsSettings: { get: (path: string) => fetchFromSettings(path, settings), - set: (skillName: string, skillInfo: Partial) => updateSkillsInSetting(skillName, skillInfo, projectId), + set: (id: string, skill: SkillSetting) => updateSkill(projectId, id, skill), }, }; diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index f2b9345a06..72ae1a51d2 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo, importResolverGenerator } from '@bfc/shared'; +import { DialogSetting, FileInfo, importResolverGenerator } from '@bfc/shared'; import { dialogIndexer } from './dialogIndexer'; import { dialogSchemaIndexer } from './dialogSchemaIndexer'; import { lgIndexer } from './lgIndexer'; import { luIndexer } from './luIndexer'; import { qnaIndexer } from './qnaIndexer'; +import { skillIndexer } from './skillIndexer'; import { skillManifestIndexer } from './skillManifestIndexer'; import { FileExtensions } from './utils/fileExtensions'; import { getExtension, getBaseName } from './utils/help'; @@ -43,7 +44,7 @@ class Indexer { return importResolverGenerator(lgFiles, '.lg', locale); }; - public index(files: FileInfo[], botName: string, locale: string) { + public index(files: FileInfo[], botName: string, locale: string, skillContent: any, settings: DialogSetting) { const result = this.classifyFile(files); return { dialogs: dialogIndexer.index(result[FileExtensions.Dialog], botName), @@ -52,6 +53,7 @@ class Indexer { luFiles: luIndexer.index(result[FileExtensions.Lu]), qnaFiles: qnaIndexer.index(result[FileExtensions.QnA]), skillManifestFiles: skillManifestIndexer.index(result[FileExtensions.Manifest]), + skills: skillIndexer.index(skillContent, settings.skill), }; } } @@ -66,3 +68,4 @@ export * from './luIndexer'; export * from './qnaIndexer'; export * from './utils'; export * from './validations'; +export * from './skillIndexer'; diff --git a/Composer/packages/lib/indexers/src/skillIndexer.ts b/Composer/packages/lib/indexers/src/skillIndexer.ts new file mode 100644 index 0000000000..67e71e81b9 --- /dev/null +++ b/Composer/packages/lib/indexers/src/skillIndexer.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Skill, SkillSetting } from '@bfc/shared'; +import pickBy from 'lodash/pickBy'; +import toPairs from 'lodash/toPairs'; + +const index = (skillContent: any[], skillSettings: { [name: string]: SkillSetting } = {}): Skill[] => { + return toPairs(skillSettings).map( + ([id, settings]): Skill => { + const { content = {} } = skillContent.find(({ id: key }) => key === id) || {}; + const { description, endpoints = [] } = content; + + return pickBy({ + id, + description, + endpoints, + content, + ...settings, + }) as Skill; + } + ); +}; + +export const skillIndexer = { + index, +}; diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index cd8a68b2a1..9416d09c0e 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -142,14 +142,14 @@ export interface LgFile { } export interface Skill { - manifestUrl: string; - name: string; - protocol: string; - description: string; - endpoints: { [key: string]: any }[]; + id: string; + content: any; + description?: string; + endpoints: any[]; endpointUrl: string; + manifestUrl: string; msAppId: string; - body: string | null | undefined; + name: string; } export interface TextFile { diff --git a/Composer/packages/lib/shared/src/types/settings.ts b/Composer/packages/lib/shared/src/types/settings.ts index 6a12c2efd2..0a549c0fb9 100644 --- a/Composer/packages/lib/shared/src/types/settings.ts +++ b/Composer/packages/lib/shared/src/types/settings.ts @@ -20,6 +20,13 @@ export interface AppUpdaterSettings { useNightly: boolean; } +export interface SkillSetting { + name: string; + manifestUrl: string; + msAppId: string; + endpointUrl: string; +} + export interface DialogSetting { MicrosoftAppId?: string; MicrosoftAppPassword?: string; @@ -34,12 +41,7 @@ export interface DialogSetting { defaultLanguage: string; languages: string[]; skill?: { - [skillName: string]: { - name: string; - manifestUrl: string; - msAppId: string; - endpointUrl: string; - }; + [skillName: string]: SkillSetting; }; botId?: string; skillHostEndpoint?: string; diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index cca88829fa..acfb580fb0 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. /* eslint-disable @typescript-eslint/no-explicit-any */ -import { DialogInfo, LuFile, LgFile, QnAFile, LuIntentSection, LgTemplate, DialogSchemaFile, Skill } from './indexers'; -import { UserSettings } from './settings'; +import { DialogInfo, LuFile, LgFile, QnAFile, LuIntentSection, LgTemplate, DialogSchemaFile } from './indexers'; +import { SkillSetting, UserSettings } from './settings'; import { OBISchema } from './schema'; /** Recursively marks all properties as optional. */ @@ -100,9 +100,9 @@ export interface ShellApi { displayManifestModal: (manifestId: string) => void; updateDialogSchema: (_: DialogSchemaFile) => Promise; createTrigger: (id: string, formData, url?: string) => void; - skillsInSettings: { + skillsSettings: { get: (path: string) => any; - set: (skillName: string, skillsData: Partial) => Promise; + set: (skillId: string, skillsData: SkillSetting) => Promise; }; } diff --git a/Composer/packages/server/__tests__/controllers/project.test.ts b/Composer/packages/server/__tests__/controllers/project.test.ts index 10f67abbaa..5ed15daf4c 100644 --- a/Composer/packages/server/__tests__/controllers/project.test.ts +++ b/Composer/packages/server/__tests__/controllers/project.test.ts @@ -331,45 +331,17 @@ describe('skill operation', () => { projectId = await BotProjectService.openProject(location2); }); - it('should check skill url', async () => { + it('should retrieve skill manifest', async () => { const mockReq = { params: { projectId }, - query: {}, - body: { + query: { url: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json', }, + body: {}, } as Request; await ProjectController.getSkill(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); }, 10000); - - it('should update skill', async () => { - const mockReq = { - params: { projectId }, - query: {}, - body: { - skills: [ - { - manifestUrl: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json', - }, - ], - }, - } as Request; - await ProjectController.updateSkill(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(200); - }, 5000); - - it('should update skill, remove', async () => { - const mockReq = { - params: { projectId }, - query: {}, - body: { - skills: [], - }, - } as Request; - await ProjectController.updateSkill(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(200); - }); }); // TODO: add a success publish test. diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 6f6c23331e..61956e4727 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -11,7 +11,7 @@ import log from '../logger'; import { BotProjectService } from '../services/project'; import AssetService from '../services/asset'; import { LocationRef } from '../models/bot/interface'; -import { getSkillByUrl } from '../models/bot/skillManager'; +import { getSkillManifest } from '../models/bot/skillManager'; import StorageService from '../services/storage'; import settings from '../settings'; @@ -252,27 +252,6 @@ async function removeFile(req: Request, res: Response) { } } -async function updateSkill(req: Request, res: Response) { - const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); - - const currentProject = await BotProjectService.getProjectById(projectId, user); - if (currentProject !== undefined) { - try { - const skills = await currentProject.updateSkill(req.body.skills); - res.status(200).json(skills); - } catch (err) { - res.status(404).json({ - message: err.message, - }); - } - } else { - res.status(404).json({ - message: 'No such bot project opened', - }); - } -} - async function getSkill(req: Request, res: Response) { const projectId = req.params.projectId; const user = await PluginLoader.getUserFromRequest(req); @@ -280,8 +259,8 @@ async function getSkill(req: Request, res: Response) { const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { try { - const skill = await getSkillByUrl(req.body.url); - res.status(200).json(skill); + const content = await getSkillManifest(req.query.url); + res.status(200).json(content); } catch (err) { res.status(404).json({ message: err.message, @@ -422,7 +401,6 @@ export const ProjectController = { updateFile, createFile, removeFile, - updateSkill, getSkill, build, setQnASettings, diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index f214875e75..abc5938af2 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -6,20 +6,10 @@ import fs from 'fs'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; -import { - getNewDesigner, - FileInfo, - Skill, - Diagnostic, - convertSkillsToDictionary, - IBotProject, - DialogSetting, - FileExtensions, -} from '@bfc/shared'; +import { getNewDesigner, FileInfo, Skill, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; import merge from 'lodash/merge'; import { UserIdentity, pluginLoader } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; -import values from 'lodash/values'; import { Path } from '../../utility/path'; import { copyDir } from '../../utility/storage'; @@ -33,7 +23,7 @@ import { BotProjectService } from '../../services/project'; import { Builder } from './builder'; import { IFileStorage } from './../storage/interface'; import { LocationRef, IBuildConfig } from './interface'; -import { extractSkillManifestUrl } from './skillManager'; +import { retrieveSkillManifests } from './skillManager'; import { defaultFilePath, serializeFiles, parseFileName } from './botStructure'; const debug = log.extend('bot-project'); @@ -149,9 +139,8 @@ export class BotProject implements IBotProject { public init = async () => { this.diagnostics = []; this.settings = await this.getEnvSettings(false); - const skillsCollection = values(this.settings?.skill); - const { skillsParsed, diagnostics } = await extractSkillManifestUrl(skillsCollection || ([] as any)); - this.skills = skillsParsed; + const { skillManifests, diagnostics } = await retrieveSkillManifests(this.settings?.skill); + this.skills = skillManifests; this.diagnostics.push(...diagnostics); this.files = await this._getFiles(); }; @@ -216,16 +205,6 @@ export class BotProject implements IBotProject { this.settings = config; }; - // update skill in settings - public updateSkill = async (config: any[]) => { - const settings = await this.getEnvSettings(false); - const { skillsParsed } = await extractSkillManifestUrl(config); - const mapped = convertSkillsToDictionary(skillsParsed); - settings.skill = await this.settingManager.set(mapped); - this.skills = skillsParsed; - return skillsParsed; - }; - public exportToZip = (cb) => { try { this.fileStorage.zip(this.dataDir, cb); diff --git a/Composer/packages/server/src/models/bot/skillManager.ts b/Composer/packages/server/src/models/bot/skillManager.ts index 0fbfefdb06..88a5576af4 100644 --- a/Composer/packages/server/src/models/bot/skillManager.ts +++ b/Composer/packages/server/src/models/bot/skillManager.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import get from 'lodash/get'; import * as msRest from '@azure/ms-rest-js'; -import { Skill, Diagnostic, DiagnosticSeverity } from '@bfc/shared'; +import { SkillSetting, Diagnostic, DiagnosticSeverity } from '@bfc/shared'; +import toPairs from 'lodash/toPairs'; import logger from './../../logger'; @@ -17,50 +17,26 @@ const token = process.env.ACCESS_TOKEN || 'token'; const creds = new msRest.TokenCredentials(token); const client = new msRest.ServiceClient(creds, clientOptions); -export const getSkillByUrl = async ( - url: string, - name?: string, - msAppId?: string, - endpointUrl?: string -): Promise => { - try { - const req: msRest.RequestPrepareOptions = { - url, - method: 'GET', - }; +export const getSkillManifest = async (url: string): Promise => { + const { bodyAsText: content } = await client.sendRequest({ + url, + method: 'GET', + }); - const res: msRest.HttpOperationResponse = await client.sendRequest(req); - - if (res.status >= 400) { - throw new Error('Manifest url can not be accessed.'); - } - - const resBody = typeof res.bodyAsText === 'string' && JSON.parse(res.bodyAsText); - return { - manifestUrl: url, - name: name || resBody?.name || '', - description: resBody?.description || '', - endpoints: get(resBody, 'endpoints', []), - endpointUrl: endpointUrl || get(resBody, 'endpoints[0].endpointUrl', ''), // needs more investment on endpoint - protocol: get(resBody, 'endpoints[0].protocol', ''), - msAppId: msAppId || get(resBody, 'endpoints[0].msAppId', ''), - body: res.bodyAsText, - }; - } catch (error) { - throw new Error('Manifest url can not be accessed.'); - } + return typeof content === 'string' ? JSON.parse(content) : {}; }; -export const extractSkillManifestUrl = async ( - skills: any[] -): Promise<{ skillsParsed: Skill[]; diagnostics: Diagnostic[] }> => { - const skillsParsed: Skill[] = []; +export const retrieveSkillManifests = async (skillSettings?: { [name: string]: SkillSetting } | SkillSetting[]) => { + const skills = toPairs(skillSettings); + const diagnostics: Diagnostic[] = []; - for (const skill of skills) { - const { manifestUrl, name, msAppId, endpointUrl } = skill; + const skillManifests: any = []; + + for (const [id, { manifestUrl }] of skills) { try { - const parsedSkill = await getSkillByUrl(manifestUrl, name, msAppId, endpointUrl); - skillsParsed.push(parsedSkill); + const content = await getSkillManifest(manifestUrl); + + skillManifests.push({ content, id, manifestUrl }); } catch (error) { const notify = new Diagnostic( `Accessing skill manifest url error, ${manifestUrl}`, @@ -68,7 +44,9 @@ export const extractSkillManifestUrl = async ( DiagnosticSeverity.Warning ); diagnostics.push(notify); + skillManifests.push({ id, manifestUrl }); } } - return { skillsParsed, diagnostics }; + + return { diagnostics, skillManifests }; }; diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index 9b2b90aa67..e28ee00cef 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -27,8 +27,7 @@ router.delete('/projects/:projectId', ProjectController.removeProject); router.put('/projects/:projectId/files/:name', ProjectController.updateFile); router.delete('/projects/:projectId/files/:name', ProjectController.removeFile); router.post('/projects/:projectId/files', ProjectController.createFile); -router.post('/projects/:projectId/skills', ProjectController.updateSkill); -router.post('/projects/:projectId/skill/check', ProjectController.getSkill); +router.get('/projects/:projectId/skill/retrieve-skill-manifest', ProjectController.getSkill); router.post('/projects/:projectId/build', ProjectController.build); router.post('/projects/:projectId/qnaSettings/set', ProjectController.setQnASettings); router.post('/projects/:projectId/project/saveAs', ProjectController.saveProjectAs); diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx index df1af439d4..5e46060a83 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx @@ -36,7 +36,7 @@ const handleBackwardCompatibility = (skills: Skill[], value): { name: string; en export const BeginSkillDialogField: React.FC = (props) => { const { depth, id, schema, uiOptions, value, onChange, definitions } = props; const { projectId, shellApi, skills = [] } = useShellApi(); - const { displayManifestModal, skillsInSettings } = shellApi; + const { displayManifestModal, skillsSettings } = shellApi; const [selectedSkill, setSelectedSkill] = useState(''); const [oldEndpoint, loadEndpointForOldBots] = useState(''); @@ -57,30 +57,29 @@ export const BeginSkillDialogField: React.FC = (props) => { } }, []); - const matchedSkill: Skill | undefined = useMemo(() => { - return skills.find(({ name }) => name === selectedSkill); + const matchedSkill = useMemo(() => { + return skills.find(({ id }) => id === selectedSkill) || ({} as Skill); }, [skills, selectedSkill]); const endpointOptions = useMemo(() => { - return (matchedSkill?.endpoints || []).map(({ name }) => name); + return (matchedSkill.endpoints || []).map(({ name }) => name); }, [matchedSkill]); const handleEndpointChange = async (skillEndpoint) => { - if (matchedSkill) { + if (matchedSkill.id) { const { msAppId, endpointUrl } = (matchedSkill.endpoints || []).find(({ name }) => name === skillEndpoint) || ({} as any); const schemaUpdate: any = {}; - const settingsUpdate: any = {}; + const settingsUpdate: any = { ...matchedSkill }; if (endpointUrl) { - skillsInSettings.set(matchedSkill.name, { endpointUrl }); - schemaUpdate.skillEndpoint = referBySettings(matchedSkill?.name, 'endpointUrl'); + schemaUpdate.skillEndpoint = referBySettings(matchedSkill.name, 'endpointUrl'); settingsUpdate.endpointUrl = endpointUrl; } if (msAppId) { - schemaUpdate.skillAppId = referBySettings(matchedSkill?.name, 'msAppId'); + schemaUpdate.skillAppId = referBySettings(matchedSkill.name, 'msAppId'); settingsUpdate.msAppId = msAppId; } - skillsInSettings.set(matchedSkill.name, { ...settingsUpdate }); + skillsSettings.set(matchedSkill.id, { ...settingsUpdate }); onChange({ ...value, ...schemaUpdate, @@ -101,7 +100,7 @@ export const BeginSkillDialogField: React.FC = (props) => { const skillEndpointUiSchema = uiOptions.properties?.skillEndpoint || {}; skillEndpointUiSchema.serializer = { get: (value) => { - const url: any = skillsInSettings.get(value); + const url: any = skillsSettings.get(value); const endpoint = (matchedSkill?.endpoints || []).find(({ endpointUrl }) => endpointUrl === url); return endpoint?.name; }, @@ -122,7 +121,7 @@ export const BeginSkillDialogField: React.FC = (props) => { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/BeginSkillDialogField.test.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/BeginSkillDialogField.test.tsx index 4208e87867..3caaab819c 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/BeginSkillDialogField.test.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/BeginSkillDialogField.test.tsx @@ -15,6 +15,8 @@ import { schema } from './constants'; const skills: any = [ { + id: 'yuesuemailskill0207', + content: {}, manifestUrl: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json', name: 'yuesuemailskill0207', endpoints: [ @@ -52,7 +54,7 @@ const renderBeginSkillDialog = ({ value = {}, onChange = jest.fn() } = {}) => { const shell: any = { addSkillDialog, - skillsInSettings: { + skillsSettings: { get: (path: string) => fetchFromSettings(path, setting), set: () => {}, }, diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx index 7b28cc1adc..09db7f078d 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx @@ -49,7 +49,7 @@ const renderSelectSkillDialog = ({ addSkillDialog, onChange } = {}) => { const shell = { addSkillDialog, - skillsInSettings: { + skillsSettings: { get: (path: string) => fetchFromSettings(path, { skill: convertSkillsToDictionary(skills), diff --git a/Composer/plugins/azurePublish/src/deploy.ts b/Composer/plugins/azurePublish/src/deploy.ts index aa94f8a621..fb95666546 100644 --- a/Composer/plugins/azurePublish/src/deploy.ts +++ b/Composer/plugins/azurePublish/src/deploy.ts @@ -42,7 +42,6 @@ export class BotProjectDeploy { profileName: string, name: string, environment: string, - language?: string, hostname?: string, luisResource?: string ) { @@ -55,6 +54,7 @@ export class BotProjectDeploy { // STEP 2: UPDATE LUIS // Do the LUIS build if LUIS settings are present + let language = settings.defaultLanguage || settings.luis.defaultLanguage; if (!language) { language = 'en-us'; } diff --git a/Composer/plugins/azurePublish/src/index.ts b/Composer/plugins/azurePublish/src/index.ts index 70fce38471..759d0370dd 100644 --- a/Composer/plugins/azurePublish/src/index.ts +++ b/Composer/plugins/azurePublish/src/index.ts @@ -28,7 +28,6 @@ interface CreateAndDeployResources { hostname?: string; luisResource?: string; subscriptionID: string; - language?: string; } interface PublishConfig { @@ -192,15 +191,7 @@ export default async (composer: any): Promise => { resourcekey: string, customizeConfiguration: CreateAndDeployResources ) => { - const { - subscriptionID, - accessToken, - name, - environment, - hostname, - luisResource, - language, - } = customizeConfiguration; + const { subscriptionID, accessToken, name, environment, hostname, luisResource } = customizeConfiguration; try { // Create the BotProjectDeploy object, which is used to carry out the deploy action. const azDeployer = new BotProjectDeploy({ @@ -221,7 +212,7 @@ export default async (composer: any): Promise => { }); // Perform the deploy - await azDeployer.deploy(project, settings, profileName, name, environment, language, hostname, luisResource); + await azDeployer.deploy(project, settings, profileName, name, environment, hostname, luisResource); // update status and history const status = this.getLoadingStatus(botId, profileName, jobId); @@ -322,7 +313,7 @@ export default async (composer: any): Promise => { environment, hostname, luisResource, - language, + defaultLanguage, settings, accessToken, } = config; @@ -360,7 +351,6 @@ export default async (composer: any): Promise => { environment, hostname, luisResource, - language, }; await this.performDeploymentAction( project,