From 51b33dbc177325188e2a9f05092993f9ff116af6 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 29 Jul 2020 15:05:39 -0700 Subject: [PATCH 001/100] Added schema for bot project support Signed-off-by: Srinaath Ravichandran --- Schemas/SampleOfficeVA.botProject.json | 24 +++++ Schemas/botproject-schema.json | 71 +++++++++++++++ Schemas/provisioningProfile-schema.json | 112 ++++++++++++++++++++++++ Schemas/sampleProvisionProfile.json | 28 ++++++ 4 files changed, 235 insertions(+) create mode 100644 Schemas/SampleOfficeVA.botProject.json create mode 100644 Schemas/botproject-schema.json create mode 100644 Schemas/provisioningProfile-schema.json create mode 100644 Schemas/sampleProvisionProfile.json diff --git a/Schemas/SampleOfficeVA.botProject.json b/Schemas/SampleOfficeVA.botProject.json new file mode 100644 index 0000000000..938622e215 --- /dev/null +++ b/Schemas/SampleOfficeVA.botProject.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", + "name": "OfficeVA", + "workspace": "file://../Projects/OfficeVA", + "provisioningProfile": "file://../Projects/DeploymentProfiles/officeVA.json", + "skills": [ + { + "workspace": "file://../Projects/RoomSchedulerSkill", + "manifest": "room-scheduler-skill-manifest-v2.1.0", + "provisioningProfile": "file://../Projects/DeploymentProfiles/roomSchedulerProfile.json", + "remote": false, + "endpointName": "local" + }, + { + "manifest": "https://calendar-skill/calendar-skill-manifest-v2.1.0.json", + "remote": true + }, + { + "workspace": "file://../Projects/LunchOrderingSkill", + "provisioningProfile": "file://../Projects/DeploymentProfiles/lunchOrderingSkillProfile.json", + "remote": false + } + ] +} diff --git a/Schemas/botproject-schema.json b/Schemas/botproject-schema.json new file mode 100644 index 0000000000..2d4356adb2 --- /dev/null +++ b/Schemas/botproject-schema.json @@ -0,0 +1,71 @@ +{ + "$id": "https://schemas.botframework.com/schemas/botprojects/v0.1/worskspace.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$version": "0.1", + "title": "Virtual Assistant and Skills workspace schema", + "description": "This is a schema that captures the relationship between the virtual assistant and its skills.", + "type": "object", + "required": ["$schema", "name", "workspace", "skills"], + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "description": "The schema to verify this workspace file against" + }, + "name": { + "type": "string", + "description": "Name of the Virtual Assistant or Root Bot." + }, + "workspace": { + "type": "string", + "description": "Path to the virtual assistant or Root Bot. Relative to the workspace file or an absolute path." + }, + "provisioningProfile": { + "type": "string", + "description": "Path to provisioning profile." + }, + "skills": { + "type": "array", + "minItems": 0, + "uniqueItems": true, + "description": "List of skills callable.", + "items": { + "$ref": "#/definitions/skill" + } + } + }, + "definitions": { + "skill": { + "type": "object", + "description": "Properties of a skill.", + "additionalProperties": false, + "required": ["manifest", "remote"], + "properties": { + "manifest": { + "type": "string", + "description": "Path to the manifest file on local file system or a URL to a remote skill." + }, + "workspace": { + "type": "string", + "description": "Path to the local skill project. If the workspace is local we use the file protocol as opposed to http/https for remote workspaces." + }, + "remote": { + "type": "string", + "description": "Indication if the skill is remote or local skill.", + "$ref": "#/definitions/booleanExpression", + "title": "isRemote", + "default": false + }, + "endpointName": { + "type": "string", + "description": "Indicates the name of the skill endpoint to use for writing the skill URL. If skill is remote no need for this property.", + "default": "default" + }, + "provisioningProfile": { + "type": "string", + "description": "Path to provisioning profile." + } + } + } + } +} diff --git a/Schemas/provisioningProfile-schema.json b/Schemas/provisioningProfile-schema.json new file mode 100644 index 0000000000..a0e15ef140 --- /dev/null +++ b/Schemas/provisioningProfile-schema.json @@ -0,0 +1,112 @@ +{ + "$id": "https://schemas.botframework.com/schemas/botprojects/v0.1/provisioning.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$version": "0.1", + "title": "Provisioning profile schema for Bot Projects", + "description": "This is a schema that captures the resource provisioning needs for a bot which maybe a Virtual Assistant or a skill", + "type": "object", + "required": [ + "$schema", + "MicrosoftAppId", + "MicrosoftAppPassword", + "accessToken" + ], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "description": "The schema to verify this provisioning profile file against" + }, + "MicrosoftAppId": { + "type": "string", + "description": "Micorosft App ID of the resource" + }, + "MicrosoftAppPassword": { + "type": "string", + "description": "Micorosft App Password of the resource" + }, + "name": { + "type": "string", + "description": "Optional name of the publishing target" + }, + "accessToken": { + "type": "string", + "description": "Access token for azure account" + }, + "applicationInsights": { + "type": "object", + "description": "Application Insights provisioning", + "properties": { + "endpointKey": { + "type": "string", + "description": "Instrumentation key for application insights" + } + } + }, + "blobStorage": { + "type": "object", + "description": "Configuring blob storage on azure", + "properties": { + "connectionString": { + "type": "string", + "description": "Connection string for blob storage" + } + } + }, + "luis": { + "type": "object", + "description": "LUIS provisioning for a bot", + "properties": { + "endpointKey": { + "type": "string", + "description": "Endpoint key for LUIS provisioning" + }, + "authoringKey": { + "type": "string", + "description": "Authoring key for LUIS provisioning" + }, + "region": { + "type": "string", + "description": "Region for LUIS provisioning" + }, + "endpoint": { + "type": "string", + "description": "LUIS endpoint configuration" + }, + "authoringEndpoint": { + "type": "string", + "description": "Authoring endpoint for LUIS" + } + } + }, + "qna": { + "type": "object", + "description": "Qna maker provisioning for a bot", + "properties": { + "knowledgebaseid": { + "type": "string", + "description": "Qna maker knowledge base id" + }, + "endpoint": { + "type": "string", + "description": "Hostname for QNA maker resource" + } + } + }, + "cosmosDb": { + "type": "object", + "description": "Cosmos db provisioning for a bot", + "properties": { + "endpoint": { + "type": "string", + "description": "Endpoint configuration for cosmos db" + }, + "authKey": { + "type": "string", + "description": "Authorization key for cosmos db" + } + } + } + } +} diff --git a/Schemas/sampleProvisionProfile.json b/Schemas/sampleProvisionProfile.json new file mode 100644 index 0000000000..9fdfd44688 --- /dev/null +++ b/Schemas/sampleProvisionProfile.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/provisioningProfile-schema.json", + "accessToken": "SDFSDFSDFSDBNASKDJASDJJLKJSADJLKMXRNZW", + "environment": "dev", + "MicrosoftAppId": "fd38ebff-02e2-427c-234234-234234", + "MicrosoftAppPassword": "XYZSASJDLKASjksdhfks!sfs", + "blobStorage": { + "connectionString": "DefaultEndpointsProtocol=https;AccountName=SampleSkill" + }, + "cosmosDb": { + "endpoint": "https://todoluissample2-dev.documents.azure.com:443/", + "authKey": "XSFDDSFOHOI" + }, + "qna": { + "knowledgebaseId": "6ea9080e0dsfsdfsdfb4c44", + "endpoint": "https://composerdemovenky.cognitiveservices.azure.com/qnamaker/v4.0/knowledgebases/89366431-a501-324324-asdhas" + }, + "luis": { + "endpointKey": "234kj23h4asdjajdkjashdkjAAsad", + "authoringKey": "234kjh890aasdhakjs", + "region": "westus", + "endpoint": "https://westus.api.cognitive.microsoft.com/", + "authoringEndpoint": "https://westus.api.cognitive.microsoft.com/" + }, + "applicationInsights": { + "endpointKey": "Asdfsdfa23432adsfsdf!sdsad" + } +} From 8d58b7cc7e7a305104f22ac960d3f59b71d621bf Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Thu, 17 Sep 2020 16:21:03 -0700 Subject: [PATCH 002/100] Added support to handle botproject space Signed-off-by: Srinaath Ravichandran --- Composer/package.json | 2 +- .../src/recoilModel/dispatchers/project.ts | 208 +++++++++++++++++- .../src/recoilModel/selectors/design.ts | 6 +- 3 files changed, 210 insertions(+), 6 deletions(-) diff --git a/Composer/package.json b/Composer/package.json index f73d7a06f0..01e987223e 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "build:plugins:runtimes": "cd plugins/runtimes && yarn install && yarn build", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", "startall": "yarn start", - "start:dev": "concurrently \"npm:start:client\" \"npm:start:server:dev\"", + "start:dev": "concurrently \"npm:start:client\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", "start:server:dev": "yarn workspace @bfc/server start:dev", diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index c5136d015a..cfa516ec75 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -1,6 +1,8 @@ /* eslint-disable react-hooks/rules-of-hooks */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { useRecoilCallback, CallbackInterface } from 'recoil'; import { dereferenceDefinitions, @@ -11,6 +13,7 @@ import { DialogSetting, convertSkillsToDictionary, } from '@bfc/shared'; +import queryString from 'query-string'; import { indexer, validateDialog } from '@bfc/indexers'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; @@ -278,9 +281,14 @@ export const projectDispatcher = () => { (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { try { await setBotOpeningStatus(callbackHelpers); - const response = await httpClient.put(`/projects/open`, { path, storageId }); - await initBotState(callbackHelpers, response.data, true, ''); - return response.data.id; + const result = await checkIfBotProject(path, storageId); + if (!result.isBotProjectSpace) { + const response = await httpClient.put(`/projects/open`, { path, storageId }); + await initBotState(callbackHelpers, response.data, true, ''); + return response.data.id; + } else { + handleBotProjectSpace(callbackHelpers, path, storageId, result.contents); + } } catch (ex) { removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); @@ -480,6 +488,200 @@ export const projectDispatcher = () => { } }); + const checkIfBotProject = async (path: string, storageId): Promise<{ isBotProjectSpace: boolean; contents: any }> => { + try { + const qs: any = { + path, + storageId, + }; + const stringified = queryString.stringify(qs, { + encode: true, + }); + const response = await httpClient.get(`/projects/checkIfBotProjectSpace?${stringified}`); + return response.data; + } catch (ex) { + return { + isBotProjectSpace: false, + contents: undefined, + }; + } + }; + + const handleBotProjectSpace = async ( + callbackHelpers: CallbackInterface, + rootBotPath: string, + storageId, + botProjectFileContents: any + ) => { + const rootBotPromise = httpClient.put(`/projects/open`, { path: rootBotPath, storageId }); + const promises = [rootBotPromise]; + for (const skill of botProjectFileContents.skills) { + if (skill.workspace) { + const { protocol } = new URL(skill.workspace); + if (protocol === 'file:') { + const relativeSkillPath = skill.workspace.replace('file://', ''); + let skillPath = path.resolve(rootBotPath, relativeSkillPath); + if (skillPath.match(/^(\/||\\)[A-Z]:/)) { + // if the path comes out like "/C:/Users", remove the leading slash or backslash + skillPath = skillPath.slice(1); + } + promises.push(httpClient.put(`/projects/open`, { path: path.normalize(skillPath), storageId })); + } + } else { + //Handle remote skill + } + } + const responses = await Promise.all(promises); + const projectIds: string[] = []; + + let rootBotData = { + projectId: '', + mainDialog: '', + }; + + const botDataCollection: any[] = []; + for (const projectData of responses) { + const projectId = projectData.data.id; + projectIds.push(projectId); + + const botData = await initBotProjectSpaceState(callbackHelpers, projectData.data, projectIds.length === 1); + + botDataCollection.push(botData); + } + // Important: gotoSnapshot will wipe all states. + const { snapshot, gotoSnapshot } = callbackHelpers; + const newSnapshot = snapshot.map(({ set }) => { + botDataCollection.map((projectData, index) => { + const projectId = projectIds[index]; + const { + skillManifestFiles, + botName, + luFiles, + dialogs, + lgFiles, + verifiedDialogs, + dialogSchemas, + botEnvironment, + qnaFiles, + curLocation, + location, + skills, + schemas, + diagnostics, + mainDialog, + locale, + isRootBot, + settings, + } = projectData; + + if (index === 0) { + rootBotData = { + projectId, + mainDialog, + }; + } + + set(skillManifestsState(projectId), skillManifestFiles); + set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); + set(lgFilesState(projectId), lgFiles); + set(dialogsState(projectId), verifiedDialogs); + set(dialogSchemasState(projectId), dialogSchemas); + set(botEnvironmentState(projectId), botEnvironment); + set(botNameState(projectId), botName); + set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); + if (location !== curLocation) { + set(botStatusState(projectId), BotStatus.unConnected); + set(locationState(projectId), location); + } + set(skillsState(projectId), skills); + set(schemasState(projectId), schemas); + set(localeState(projectId), locale); + set(botDiagnosticsState(projectId), diagnostics); + + refreshLocalStorage(projectId, settings); + const mergedSettings = mergeLocalStorage(projectId, settings); + set(settingsState(projectId), mergedSettings); + set(filePersistenceState(projectId), new FilePersistence(projectId)); + set(undoHistoryState(projectId), new UndoHistory(projectId)); + set(projectMetaDataState(projectId), { + isRootBot, + }); + }); + set(botProjectsSpaceState, [...projectIds]); + }); + gotoSnapshot(newSnapshot); + + if (rootBotData.projectId) { + const url = `/bot/${rootBotData.projectId}/dialogs/${rootBotData.mainDialog}`; + navigateTo(url); + } + }; + + const initBotProjectSpaceState = async (callbackHelpers: CallbackInterface, data: any, isRootBot = false) => { + const { snapshot } = callbackHelpers; + const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data; + const curLocation = await snapshot.getPromise(locationState(projectId)); + const storedLocale = languageStorage.get(botName)?.locale; + const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; + + // cache current projectId in session, resolve page refresh caused state lost. + if (isRootBot) { + projectIdCache.set(projectId); + } + + try { + schemas.sdk.content = processSchema(projectId, schemas.sdk.content); + } catch (err) { + const diagnostics = schemas.diagnostics ?? []; + diagnostics.push(err.message); + schemas.diagnostics = diagnostics; + } + + try { + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles } = indexer.index( + files, + botName, + locale + ); + + let mainDialog = ''; + const verifiedDialogs = dialogs.map((dialog) => { + if (dialog.isRoot) { + mainDialog = dialog.id; + } + dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); + return dialog; + }); + + await lgWorker.addProject(projectId, lgFiles); + + return { + skillManifestFiles, + botName, + luFiles, + dialogs, + lgFiles, + verifiedDialogs, + dialogSchemas, + botEnvironment, + qnaFiles, + curLocation, + location, + skills, + schemas, + diagnostics, + mainDialog, + settings, + locale, + isRootBot, + }; + } catch (err) { + setError(callbackHelpers, err); + navigateTo('/home'); + return ''; + } + }; + return { openProject, createProject, diff --git a/Composer/packages/client/src/recoilModel/selectors/design.ts b/Composer/packages/client/src/recoilModel/selectors/design.ts index 7814bdfd45..8ffca593eb 100644 --- a/Composer/packages/client/src/recoilModel/selectors/design.ts +++ b/Composer/packages/client/src/recoilModel/selectors/design.ts @@ -3,7 +3,7 @@ import { selector } from 'recoil'; -import { botNameState, botProjectsSpaceState } from '../atoms'; +import { botNameState, botProjectsSpaceState, dialogsState } from '../atoms'; //TODO: This selector will be used when BotProjects is implemented export const botProjectSpaceSelector = selector({ @@ -11,8 +11,10 @@ export const botProjectSpaceSelector = selector({ get: ({ get }) => { const botProjects = get(botProjectsSpaceState); const result = botProjects.map((botProjectId: string) => { + const dialogs = get(dialogsState(botProjectId)); const name = get(botNameState(botProjectId)); - return { projectId: botProjectId, name }; + const projectId = botProjectId; + return { dialogs, projectId, name }; }); return result; }, From 7041fd34fd1f5ed25d93e4de0ac155da6ff0cc0d Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 18 Sep 2020 09:48:31 -0700 Subject: [PATCH 003/100] More updates Signed-off-by: Srinaath Ravichandran --- .../client/src/recoilModel/atoms/appState.ts | 5 ---- .../src/recoilModel/dispatchers/project.ts | 25 ++++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 3acebf35ef..6e5db5381a 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -157,11 +157,6 @@ export const extensionsState = atom({ default: [], }); -export const botOpeningState = atom({ - key: getFullyQualifiedKey('botOpening'), - default: false, -}); - export const botProjectsSpaceState = atom({ key: getFullyQualifiedKey('botProjectsSpace'), default: [], diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index cfa516ec75..3303fa2817 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -60,7 +60,6 @@ import { botNameState, botEnvironmentState, dialogsState, - botOpeningState, recentProjectsState, templateProjectsState, runtimeTemplatesState, @@ -266,7 +265,6 @@ export const projectDispatcher = () => { const setBotOpeningStatus = async (callbackHelpers: CallbackInterface) => { const { set, snapshot } = callbackHelpers; - set(botOpeningState, true); const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState); const filePersistenceHandlers: filePersistence[] = []; for (const projectId of botProjectSpace) { @@ -296,6 +294,25 @@ export const projectDispatcher = () => { } ); + const openProjectTest = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { + try { + await setBotOpeningStatus(callbackHelpers); + const result = await checkIfBotProject(path, storageId); + if (!result.isBotProjectSpace) { + const response = await httpClient.put(`/projects/open`, { path, storageId }); + await initBotState(callbackHelpers, response.data, true, ''); + return response.data.id; + } else { + handleBotProjectSpace(callbackHelpers, path, storageId, result.contents); + } + } catch (ex) { + removeRecentProject(callbackHelpers, path); + handleProjectFailure(callbackHelpers, ex); + } + } + ); + const fetchProjectById = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { try { const response = await httpClient.get(`/projects/${projectId}`); @@ -544,7 +561,7 @@ export const projectDispatcher = () => { const projectId = projectData.data.id; projectIds.push(projectId); - const botData = await initBotProjectSpaceState(callbackHelpers, projectData.data, projectIds.length === 1); + const botData = await fetchData(callbackHelpers, projectData.data, projectIds.length === 1); botDataCollection.push(botData); } @@ -617,7 +634,7 @@ export const projectDispatcher = () => { } }; - const initBotProjectSpaceState = async (callbackHelpers: CallbackInterface, data: any, isRootBot = false) => { + const fetchData = async (callbackHelpers: CallbackInterface, data: any, isRootBot = false) => { const { snapshot } = callbackHelpers; const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data; const curLocation = await snapshot.getPromise(locationState(projectId)); From 425df2e952ddf5b94ab05c2a0662204ba1a2508c Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 18 Sep 2020 16:24:58 -0700 Subject: [PATCH 004/100] temp Signed-off-by: Srinaath Ravichandran Loading multiple projects working effeiciently Signed-off-by: Srinaath Ravichandran --- .../client/src/pages/design/DesignPage.tsx | 6 + .../client/src/recoilModel/atoms/appState.ts | 5 + .../recoilModel/dispatchers/project-backup.ts | 280 +++++++++++++ .../src/recoilModel/dispatchers/project.ts | 373 ++++-------------- .../recoilModel/dispatchers/utils/project.ts | 137 +++++++ .../indexers/src/botProjectSpaceIndexer.ts | 17 + Composer/packages/lib/indexers/src/index.ts | 4 + .../lib/indexers/src/utils/fileExtensions.ts | 1 + .../packages/lib/shared/src/types/indexers.ts | 13 + .../server/src/models/bot/botProject.ts | 1 + 10 files changed, 538 insertions(+), 299 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts create mode 100644 Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 09754e1e46..a3ef5589b6 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -48,6 +48,7 @@ import { showAddSkillDialogModalState, actionsSeedState, localeState, + botProjectSpaceSelector, } from '../../recoilModel'; import ImportQnAFromUrlModal from '../knowledge-base/ImportQnAFromUrlModal'; import { triggerNotSupported } from '../../utils/dialogValidator'; @@ -109,6 +110,7 @@ const getTabFromFragment = () => { const DesignPage: React.FC> = (props) => { const { location, dialogId, projectId = '' } = props; const userSettings = useRecoilValue(userSettingsState); + const botProjectsSpace = useRecoilValue(botProjectSpaceSelector); const schemas = useRecoilValue(schemasState(projectId)); const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); @@ -156,6 +158,10 @@ const DesignPage: React.FC { + console.log(botProjectsSpace); + }, [botProjectsSpace]); + useEffect(() => { const currentDialog = dialogs.find(({ id }) => id === dialogId); if (currentDialog) { diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 6e5db5381a..06179b9134 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -166,3 +166,8 @@ export const currentProjectIdState = atom({ key: getFullyQualifiedKey('currentProjectId'), default: '', }); + +export const botOpeningState = atom({ + key: getFullyQualifiedKey('botOpening'), + default: false, +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts b/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts new file mode 100644 index 0000000000..13e02dd619 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts @@ -0,0 +1,280 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import path from 'path'; + +import { useRecoilCallback, CallbackInterface } from 'recoil'; +import { + dereferenceDefinitions, + LuFile, + QnAFile, + DialogInfo, + SensitiveProperties, + DialogSetting, + convertSkillsToDictionary, +} from '@bfc/shared'; +import queryString from 'query-string'; +import { indexer, validateDialog } from '@bfc/indexers'; +import objectGet from 'lodash/get'; +import objectSet from 'lodash/set'; +import formatMessage from 'format-message'; + +import lgWorker from '../parsers/lgWorker'; +import luWorker from '../parsers/luWorker'; +import qnaWorker from '../parsers/qnaWorker'; +import httpClient from '../../utils/httpUtil'; +import { BotStatus } from '../../constants'; +import { getReferredLuFiles } from '../../utils/luUtil'; +import luFileStatusStorage from '../../utils/luFileStatusStorage'; +import { getReferredQnaFiles } from '../../utils/qnaUtil'; +import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; +import settingStorage from '../../utils/dialogSettingStorage'; +import filePersistence from '../persistence/FilePersistence'; +import { navigateTo } from '../../utils/navigation'; +import languageStorage from '../../utils/languageStorage'; +import { projectIdCache } from '../../utils/projectCache'; +import { + designPageLocationState, + botDiagnosticsState, + botProjectsSpaceState, + projectMetaDataState, + filePersistenceState, + currentProjectIdState, + botOpeningState, +} from '../atoms'; +import { QnABotTemplateId } from '../../constants'; +import FilePersistence from '../persistence/FilePersistence'; +import UndoHistory from '../undo/undoHistory'; +import { undoHistoryState } from '../undo/history'; + +import { + skillManifestsState, + settingsState, + localeState, + luFilesState, + qnaFilesState, + skillsState, + schemasState, + lgFilesState, + locationState, + botStatusState, + botNameState, + botEnvironmentState, + dialogsState, + recentProjectsState, + templateProjectsState, + runtimeTemplatesState, + applicationErrorState, + templateIdState, + announcementState, + boilerplateVersionState, + dialogSchemasState, +} from './../atoms'; +import { logMessage, setError } from './../dispatchers/shared'; +import { + fetchProjectDataByPath, + fetchProjectDataById, + flushExistingTasks, + getMergedSettings, + navigateToBot, + parseSkillPaths, +} from './utils/project'; + +const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { + callbackHelpers.set(botOpeningState, false); + setError(callbackHelpers, ex); +}; + +const removeRecentProject = async (callbackHelpers: CallbackInterface, path: string) => { + try { + const { + set, + snapshot: { getPromise }, + } = callbackHelpers; + const currentRecentProjects = await getPromise(recentProjectsState); + const filtered = currentRecentProjects.filter((p) => p.path !== path); + set(recentProjectsState, filtered); + } catch (ex) { + logMessage(callbackHelpers, `Error removing recent project: ${ex}`); + } +}; + +const processSchema = (projectId: string, schema: any) => ({ + ...schema, + definitions: dereferenceDefinitions(schema.definitions), +}); + +// if user set value in terminal or appsetting.json, it should update the value in localStorage +const refreshLocalStorage = (projectId: string, settings: DialogSetting) => { + for (const property of SensitiveProperties) { + const value = objectGet(settings, property); + if (value) { + settingStorage.setField(projectId, property, value); + } + } +}; + +// merge sensitive values in localStorage +const mergeLocalStorage = (projectId: string, settings: DialogSetting) => { + const localSetting = settingStorage.get(projectId); + const mergedSettings = { ...settings }; + if (localSetting) { + for (const property of SensitiveProperties) { + const value = objectGet(localSetting, property); + if (value) { + objectSet(mergedSettings, property, value); + } else { + objectSet(mergedSettings, property, ''); // set those key back, because that were omit after persisited + } + } + } + return mergedSettings; +}; + +const updateLuFilesStatus = (projectId: string, luFiles: LuFile[]) => { + const status = luFileStatusStorage.get(projectId); + return luFiles.map((luFile) => { + if (typeof status[luFile.id] === 'boolean') { + return { ...luFile, published: status[luFile.id] }; + } else { + return { ...luFile, published: false }; + } + }); +}; + +const initLuFilesStatus = (projectId: string, luFiles: LuFile[], dialogs: DialogInfo[]) => { + luFileStatusStorage.checkFileStatus( + projectId, + getReferredLuFiles(luFiles, dialogs).map((file) => file.id) + ); + return updateLuFilesStatus(projectId, luFiles); +}; + +const updateQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[]) => { + const status = qnaFileStatusStorage.get(projectId); + return qnaFiles.map((qnaFile) => { + if (typeof status[qnaFile.id] === 'boolean') { + return { ...qnaFile, published: status[qnaFile.id] }; + } else { + return { ...qnaFile, published: false }; + } + }); +}; + +const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: DialogInfo[]) => { + qnaFileStatusStorage.checkFileStatus( + projectId, + getReferredQnaFiles(qnaFiles, dialogs).map((file) => file.id) + ); + return updateQnaFilesStatus(projectId, qnaFiles); +}; + +export const projectDispatcher = () => { + const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { + const { snapshot, set } = callbackHelpers; + const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; + const curLocation = await snapshot.getPromise(locationState(projectId)); + const storedLocale = languageStorage.get(botName)?.locale; + const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; + + // cache current projectId in session, resolve page refresh caused state lost. + projectIdCache.set(projectId); + + try { + schemas.sdk.content = processSchema(projectId, schemas.sdk.content); + } catch (err) { + const diagnostics = schemas.diagnostics ?? []; + diagnostics.push(err.message); + schemas.diagnostics = diagnostics; + } + + try { + let mainDialog = ''; + const verifiedDialogs = dialogs.map((dialog) => { + if (dialog.isRoot) { + mainDialog = dialog.id; + } + dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); + return dialog; + }); + + await lgWorker.addProject(projectId, lgFiles); + set(botProjectsSpaceState, []); + + set(skillManifestsState(projectId), skillManifestFiles); + set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); + set(lgFilesState(projectId), lgFiles); + set(dialogsState(projectId), verifiedDialogs); + set(dialogSchemasState(projectId), dialogSchemas); + set(botEnvironmentState(projectId), botEnvironment); + set(botNameState(projectId), botName); + set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); + if (location !== curLocation) { + set(botStatusState(projectId), BotStatus.unConnected); + set(locationState(projectId), location); + } + set(skillsState(projectId), skills); + set(schemasState(projectId), schemas); + set(localeState(projectId), locale); + set(botDiagnosticsState(projectId), diagnostics); + + refreshLocalStorage(projectId, settings); + set(settingsState(projectId), mergedSettings); + set(filePersistenceState(projectId), new FilePersistence(projectId)); + set(undoHistoryState(projectId), new UndoHistory(projectId)); + set(projectMetaDataState(projectId), { + isRootBot, + }); + return mainDialog; + } catch (err) { + callbackHelpers.set(botOpeningState, false); + setError(callbackHelpers, err); + navigateTo('/home'); + return ''; + } + }; + + const openSkill = async (callbackHelpers, skill: { path: string; remote: boolean }, storageId) => { + const { set } = callbackHelpers; + try { + if (!skill.remote) { + const { projectData, botFiles } = await fetchProjectDataByPath(skill.path, storageId); + await initBotState(callbackHelpers, projectData, botFiles); + set(botProjectsSpaceState, (current) => [...current, projectData.projectId]); + } + } catch (ex) { + // Handle exception in opening a skill + } + }; + + const openProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { + try { + const { set } = callbackHelpers; + await flushExistingTasks(callbackHelpers); + const { projectData, botFiles } = await fetchProjectDataByPath(path, storageId); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); + set(botProjectsSpaceState, []); + if (botFiles.botProjectSpaceFiles.length) { + // Handle botproject space code here. CUrrently always fetching the first BotProject file received. In future, there would be a file for each environment + const skillsInBotProject = parseSkillPaths(botFiles.botProjectSpaceFiles[0]); + skillsInBotProject.forEach((skillInBotProject) => { + openSkill(callbackHelpers, skillInBotProject, storageId); + }); + } + //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject + set(botProjectsSpaceState, [projectData.projectId]); + set(currentProjectIdState, projectData.projectId); + navigateToBot(projectData.projectId, mainDialog); + } catch (ex) { + removeRecentProject(callbackHelpers, path); + handleProjectFailure(callbackHelpers, ex); + } + } + ); + + return { + openProject, + }; +}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 8b7c71da5e..e5eb6a3c03 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -40,6 +40,7 @@ import { projectMetaDataState, filePersistenceState, currentProjectIdState, + botOpeningState, } from '../atoms'; import { QnABotTemplateId } from '../../constants'; import FilePersistence from '../persistence/FilePersistence'; @@ -70,6 +71,13 @@ import { dialogSchemasState, } from './../atoms'; import { logMessage, setError } from './../dispatchers/shared'; +import { + fetchProjectDataById, + fetchProjectDataByPath, + flushExistingTasks, + navigateToBot, + parseSkillPaths, +} from './utils/project'; const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { callbackHelpers.set(botOpeningState, false); @@ -147,25 +155,10 @@ const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: Dia }; export const projectDispatcher = () => { - const initBotState = async ( - callbackHelpers: CallbackInterface, - data: any, - jump: boolean, - templateId: string, - qnaKbUrls?: string[] - ) => { - const { snapshot, gotoSnapshot, set } = callbackHelpers; - const { - files, - botName, - botEnvironment, - location, - schemas, - settings, - id: projectId, - diagnostics, - skills: skillContent, - } = data; + const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { + const { snapshot, set } = callbackHelpers; + const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; const curLocation = await snapshot.getPromise(locationState(projectId)); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; @@ -173,12 +166,6 @@ 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) { @@ -188,14 +175,6 @@ export const projectDispatcher = () => { } try { - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills } = indexer.index( - files, - botName, - locale, - skillContent, - mergedSettings - ); - let mainDialog = ''; const verifiedDialogs = dialogs.map((dialog) => { if (dialog.isRoot) { @@ -206,56 +185,37 @@ export const projectDispatcher = () => { }); await lgWorker.addProject(projectId, lgFiles); - set(botProjectsSpaceState, []); - // Important: gotoSnapshot will wipe all states. - const newSnapshot = snapshot.map(({ set }) => { - set(skillManifestsState(projectId), skillManifestFiles); - set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); - set(lgFilesState(projectId), lgFiles); - set(dialogsState(projectId), verifiedDialogs); - set(dialogSchemasState(projectId), dialogSchemas); - set(botEnvironmentState(projectId), botEnvironment); - set(botNameState(projectId), botName); - set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); - if (location !== curLocation) { - set(botStatusState(projectId), BotStatus.unConnected); - set(locationState(projectId), location); - } - set(skillsState(projectId), skills); - set(schemasState(projectId), schemas); - set(localeState(projectId), locale); - set(botDiagnosticsState(projectId), diagnostics); - - refreshLocalStorage(projectId, settings); - set(settingsState(projectId), mergedSettings); - set(filePersistenceState(projectId), new FilePersistence(projectId)); - set(undoHistoryState(projectId), new UndoHistory(projectId)); - //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject - set(botProjectsSpaceState, (current) => [...current, projectId]); - set(projectMetaDataState(projectId), { - isRootBot: true, - }); - set(botOpeningState, false); - }); - - gotoSnapshot(newSnapshot); - - if (jump && projectId) { - // TODO: Refactor to set it always on init to the root bot - set(currentProjectIdState, projectId); - let url = `/bot/${projectId}/dialogs/${mainDialog}`; - if (templateId === QnABotTemplateId) { - url = `/bot/${projectId}/knowledge-base/${mainDialog}`; - navigateTo(url, { state: { qnaKbUrls } }); - return; - } - navigateTo(url); + set(skillManifestsState(projectId), skillManifestFiles); + set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); + set(lgFilesState(projectId), lgFiles); + set(dialogsState(projectId), verifiedDialogs); + set(dialogSchemasState(projectId), dialogSchemas); + set(botEnvironmentState(projectId), botEnvironment); + set(botNameState(projectId), botName); + set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); + if (location !== curLocation) { + set(botStatusState(projectId), BotStatus.unConnected); + set(locationState(projectId), location); } + set(skillsState(projectId), skills); + set(schemasState(projectId), schemas); + set(localeState(projectId), locale); + set(botDiagnosticsState(projectId), diagnostics); + + refreshLocalStorage(projectId, settings); + set(settingsState(projectId), mergedSettings); + set(filePersistenceState(projectId), new FilePersistence(projectId)); + set(undoHistoryState(projectId), new UndoHistory(projectId)); + set(projectMetaDataState(projectId), { + isRootBot, + }); + return mainDialog; } catch (err) { callbackHelpers.set(botOpeningState, false); setError(callbackHelpers, err); navigateTo('/home'); + return ''; } }; @@ -275,6 +235,7 @@ export const projectDispatcher = () => { const setBotOpeningStatus = async (callbackHelpers: CallbackInterface) => { const { set, snapshot } = callbackHelpers; + set(botOpeningState, true); const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState); const filePersistenceHandlers: filePersistence[] = []; for (const projectId of botProjectSpace) { @@ -285,37 +246,42 @@ export const projectDispatcher = () => { return Promise.all(workers.map((w) => w.flush())); }; - const openProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { - try { - await setBotOpeningStatus(callbackHelpers); - const result = await checkIfBotProject(path, storageId); - if (!result.isBotProjectSpace) { - const response = await httpClient.put(`/projects/open`, { path, storageId }); - await initBotState(callbackHelpers, response.data, true, ''); - return response.data.id; - } else { - handleBotProjectSpace(callbackHelpers, path, storageId, result.contents); - } - } catch (ex) { - removeRecentProject(callbackHelpers, path); - handleProjectFailure(callbackHelpers, ex); + const openSkill = async (callbackHelpers, skill: { path: string; remote: boolean }, storageId) => { + const { set } = callbackHelpers; + try { + if (!skill.remote) { + const { projectData, botFiles } = await fetchProjectDataByPath(skill.path, storageId); + await initBotState(callbackHelpers, projectData, botFiles); + set(botProjectsSpaceState, (current) => [...current, projectData.id]); } + } catch (ex) { + // Handle exception in opening a skill } - ); + }; + + const handleBotOpening = async (callbackHelpers, projectData, botFiles, storageId) => { + const { set } = callbackHelpers; + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); + set(botProjectsSpaceState, []); + if (botFiles.botProjectSpaceFiles.length) { + // Handle botproject space code here. CUrrently always fetching the first BotProject file received. In future, there would be a file for each environment + const skillsInBotProject = parseSkillPaths(botFiles.botProjectSpaceFiles[0]); + skillsInBotProject.forEach((skillInBotProject) => { + openSkill(callbackHelpers, skillInBotProject, storageId); + }); + } + //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject + set(botProjectsSpaceState, [projectData.id]); + set(currentProjectIdState, projectData.id); + navigateToBot(projectData.id, mainDialog); + }; - const openProjectTest = useRecoilCallback( + const openProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { try { - await setBotOpeningStatus(callbackHelpers); - const result = await checkIfBotProject(path, storageId); - if (!result.isBotProjectSpace) { - const response = await httpClient.put(`/projects/open`, { path, storageId }); - await initBotState(callbackHelpers, response.data, true, ''); - return response.data.id; - } else { - handleBotProjectSpace(callbackHelpers, path, storageId, result.contents); - } + await flushExistingTasks(callbackHelpers); + const { projectData, botFiles } = await fetchProjectDataByPath(path, storageId); + handleBotOpening(callbackHelpers, projectData, botFiles, storageId); } catch (ex) { removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); @@ -325,8 +291,11 @@ export const projectDispatcher = () => { const fetchProjectById = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { try { - const response = await httpClient.get(`/projects/${projectId}`); - await initBotState(callbackHelpers, response.data, false, ''); + const { set } = callbackHelpers; + await flushExistingTasks(callbackHelpers); + set(botProjectsSpaceState, []); + const { projectData, botFiles } = await fetchProjectDataById(projectId); + handleBotOpening(callbackHelpers, projectData, botFiles, 'default'); } catch (ex) { handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); @@ -515,200 +484,6 @@ export const projectDispatcher = () => { } }); - const checkIfBotProject = async (path: string, storageId): Promise<{ isBotProjectSpace: boolean; contents: any }> => { - try { - const qs: any = { - path, - storageId, - }; - const stringified = queryString.stringify(qs, { - encode: true, - }); - const response = await httpClient.get(`/projects/checkIfBotProjectSpace?${stringified}`); - return response.data; - } catch (ex) { - return { - isBotProjectSpace: false, - contents: undefined, - }; - } - }; - - const handleBotProjectSpace = async ( - callbackHelpers: CallbackInterface, - rootBotPath: string, - storageId, - botProjectFileContents: any - ) => { - const rootBotPromise = httpClient.put(`/projects/open`, { path: rootBotPath, storageId }); - const promises = [rootBotPromise]; - for (const skill of botProjectFileContents.skills) { - if (skill.workspace) { - const { protocol } = new URL(skill.workspace); - if (protocol === 'file:') { - const relativeSkillPath = skill.workspace.replace('file://', ''); - let skillPath = path.resolve(rootBotPath, relativeSkillPath); - if (skillPath.match(/^(\/||\\)[A-Z]:/)) { - // if the path comes out like "/C:/Users", remove the leading slash or backslash - skillPath = skillPath.slice(1); - } - promises.push(httpClient.put(`/projects/open`, { path: path.normalize(skillPath), storageId })); - } - } else { - //Handle remote skill - } - } - const responses = await Promise.all(promises); - const projectIds: string[] = []; - - let rootBotData = { - projectId: '', - mainDialog: '', - }; - - const botDataCollection: any[] = []; - for (const projectData of responses) { - const projectId = projectData.data.id; - projectIds.push(projectId); - - const botData = await fetchData(callbackHelpers, projectData.data, projectIds.length === 1); - - botDataCollection.push(botData); - } - // Important: gotoSnapshot will wipe all states. - const { snapshot, gotoSnapshot } = callbackHelpers; - const newSnapshot = snapshot.map(({ set }) => { - botDataCollection.map((projectData, index) => { - const projectId = projectIds[index]; - const { - skillManifestFiles, - botName, - luFiles, - dialogs, - lgFiles, - verifiedDialogs, - dialogSchemas, - botEnvironment, - qnaFiles, - curLocation, - location, - skills, - schemas, - diagnostics, - mainDialog, - locale, - isRootBot, - settings, - } = projectData; - - if (index === 0) { - rootBotData = { - projectId, - mainDialog, - }; - } - - set(skillManifestsState(projectId), skillManifestFiles); - set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); - set(lgFilesState(projectId), lgFiles); - set(dialogsState(projectId), verifiedDialogs); - set(dialogSchemasState(projectId), dialogSchemas); - set(botEnvironmentState(projectId), botEnvironment); - set(botNameState(projectId), botName); - set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); - if (location !== curLocation) { - set(botStatusState(projectId), BotStatus.unConnected); - set(locationState(projectId), location); - } - set(skillsState(projectId), skills); - set(schemasState(projectId), schemas); - set(localeState(projectId), locale); - set(botDiagnosticsState(projectId), diagnostics); - - refreshLocalStorage(projectId, settings); - const mergedSettings = mergeLocalStorage(projectId, settings); - set(settingsState(projectId), mergedSettings); - set(filePersistenceState(projectId), new FilePersistence(projectId)); - set(undoHistoryState(projectId), new UndoHistory(projectId)); - set(projectMetaDataState(projectId), { - isRootBot, - }); - }); - set(botProjectsSpaceState, [...projectIds]); - }); - gotoSnapshot(newSnapshot); - - if (rootBotData.projectId) { - const url = `/bot/${rootBotData.projectId}/dialogs/${rootBotData.mainDialog}`; - navigateTo(url); - } - }; - - const fetchData = async (callbackHelpers: CallbackInterface, data: any, isRootBot = false) => { - const { snapshot } = callbackHelpers; - const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data; - const curLocation = await snapshot.getPromise(locationState(projectId)); - const storedLocale = languageStorage.get(botName)?.locale; - const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; - - // cache current projectId in session, resolve page refresh caused state lost. - if (isRootBot) { - projectIdCache.set(projectId); - } - - try { - schemas.sdk.content = processSchema(projectId, schemas.sdk.content); - } catch (err) { - const diagnostics = schemas.diagnostics ?? []; - diagnostics.push(err.message); - schemas.diagnostics = diagnostics; - } - - try { - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles } = indexer.index( - files, - botName, - locale - ); - - let mainDialog = ''; - const verifiedDialogs = dialogs.map((dialog) => { - if (dialog.isRoot) { - mainDialog = dialog.id; - } - dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); - return dialog; - }); - - await lgWorker.addProject(projectId, lgFiles); - - return { - skillManifestFiles, - botName, - luFiles, - dialogs, - lgFiles, - verifiedDialogs, - dialogSchemas, - botEnvironment, - qnaFiles, - curLocation, - location, - skills, - schemas, - diagnostics, - mainDialog, - settings, - locale, - isRootBot, - }; - } catch (err) { - setError(callbackHelpers, err); - navigateTo('/home'); - return ''; - } - }; - return { openProject, createProject, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts new file mode 100644 index 0000000000..eb9458e71a --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import path from 'path'; + +import { SensitiveProperties, convertSkillsToDictionary, DialogSetting, BotProjectSpaceSkill } from '@bfc/shared'; +import objectGet from 'lodash/get'; +import objectSet from 'lodash/set'; +import { BotProjectSpace } from '@bfc/shared/src/types'; +import { indexer } from '@bfc/indexers'; + +import languageStorage from '../../../utils/languageStorage'; +import settingStorage from '../../../utils/dialogSettingStorage'; +import { botProjectsSpaceState, filePersistenceState } from '../../atoms'; +import lgWorker from '../../parsers/lgWorker'; +import luWorker from '../../parsers/luWorker'; +import qnaWorker from '../../parsers/qnaWorker'; +import FilePersistence from '../../persistence/FilePersistence'; +import { navigateTo } from '../../../utils/navigation'; +import { QnABotTemplateId } from '../../../constants'; +import httpClient from '../../../utils/httpUtil'; + +export const flushExistingTasks = async (callbackHelpers) => { + const { snapshot } = callbackHelpers; + const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState); + const filePersistenceHandlers: FilePersistence[] = []; + for (const projectId of botProjectSpace) { + const fp = await snapshot.getPromise(filePersistenceState(projectId)); + filePersistenceHandlers.push(fp); + } + const workers = [lgWorker, luWorker, qnaWorker, ...filePersistenceHandlers]; + return Promise.all(workers.map((w) => w.flush())); +}; + +// merge sensitive values in localStorage +const mergeLocalStorage = (projectId: string, settings: DialogSetting) => { + const localSetting = settingStorage.get(projectId); + const mergedSettings = { ...settings }; + if (localSetting) { + for (const property of SensitiveProperties) { + const value = objectGet(localSetting, property); + if (value) { + objectSet(mergedSettings, property, value); + } else { + objectSet(mergedSettings, property, ''); // set those key back, because that were omit after persisited + } + } + } + return mergedSettings; +}; + +export const getMergedSettings = (projectId, settings): DialogSetting => { + const mergedSettings = mergeLocalStorage(projectId, settings); + if (Array.isArray(mergedSettings.skill)) { + const skillsArr = mergedSettings.skill.map((skillData) => ({ ...skillData })); + mergedSettings.skill = convertSkillsToDictionary(skillsArr); + } + return mergedSettings; +}; + +export const navigateToBot = (projectId: string, mainDialog: string, qnaKbUrls?: string[], templateId?: string) => { + if (projectId) { + let url = `/bot/${projectId}/dialogs/${mainDialog}`; + if (templateId === QnABotTemplateId) { + url = `/bot/${projectId}/knowledge-base/${mainDialog}`; + navigateTo(url, { state: { qnaKbUrls } }); + return; + } + navigateTo(url); + } +}; + +const loadProjectData = (response) => { + const { files, botName, settings, skills: skillContent, id: projectId } = response.data; + const mergedSettings = getMergedSettings(projectId, settings); + const storedLocale = languageStorage.get(botName)?.locale; + const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; + const indexedFiles = indexer.index(files, botName, locale, skillContent, mergedSettings); + return { + botFiles: { ...indexedFiles, mergedSettings }, + projectData: response.data, + error: undefined, + }; +}; + +export const fetchProjectDataByPath = async ( + path, + storageId +): Promise<{ botFiles: any; projectData: any; error: any }> => { + try { + const response = await httpClient.put(`/projects/open`, { path, storageId }); + const projectData = loadProjectData(response); + return projectData; + } catch (ex) { + return { + botFiles: undefined, + projectData: undefined, + error: ex, + }; + } +}; + +export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { + try { + const response = await httpClient.get(`/projects/${projectId}`); + const projectData = loadProjectData(response); + return projectData; + } catch (ex) { + return { + botFiles: undefined, + projectData: undefined, + error: ex, + }; + } +}; + +export const parseSkillPaths = (botProjectSpace: BotProjectSpace): { path: string; remote: boolean }[] => { + const rootBotPath = botProjectSpace.workspace.replace('file://', ''); + const result = botProjectSpace.skills.map((skill: BotProjectSpaceSkill) => { + if (skill.workspace) { + const { protocol } = new URL(skill.workspace); + if (protocol === 'file:') { + const relativeSkillPath = skill.workspace.replace('file://', ''); + const skillPath = path.resolve(rootBotPath, relativeSkillPath); + return { + path: path.normalize(skillPath), + remote: false, + }; + } + } + return { + path: skill.manifest, + remote: false, + }; + }); + return result; +}; diff --git a/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts b/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts new file mode 100644 index 0000000000..a47050c696 --- /dev/null +++ b/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BotProjectSpace, FileInfo } from '@bfc/shared'; + +const index = (botProjectSpaceFiles: FileInfo[]) => { + // Handle botproject files for multiple env when Composer brings in Env + return botProjectSpaceFiles.map((file) => { + const { content } = file; + const jsonContent: BotProjectSpace = JSON.parse(content); + return jsonContent; + }); +}; + +export const botProjectSpaceIndexer = { + index, +}; diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index 72ae1a51d2..bbf1b42848 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -9,6 +9,7 @@ import { luIndexer } from './luIndexer'; import { qnaIndexer } from './qnaIndexer'; import { skillIndexer } from './skillIndexer'; import { skillManifestIndexer } from './skillManifestIndexer'; +import { botProjectSpaceIndexer } from './botProjectSpaceIndexer'; import { FileExtensions } from './utils/fileExtensions'; import { getExtension, getBaseName } from './utils/help'; @@ -29,6 +30,7 @@ class Indexer { [FileExtensions.Dialog]: [], [FileExtensions.DialogSchema]: [], [FileExtensions.Manifest]: [], + [FileExtensions.BotProjectSpace]: [], } ); } @@ -54,6 +56,7 @@ class Indexer { qnaFiles: qnaIndexer.index(result[FileExtensions.QnA]), skillManifestFiles: skillManifestIndexer.index(result[FileExtensions.Manifest]), skills: skillIndexer.index(skillContent, settings.skill), + botProjectSpaceFiles: botProjectSpaceIndexer.index(result[FileExtensions.BotProjectSpace]), }; } } @@ -69,3 +72,4 @@ export * from './qnaIndexer'; export * from './utils'; export * from './validations'; export * from './skillIndexer'; +export * from './botProjectSpaceIndexer'; diff --git a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts index bb77af52c8..bea3ea3ab3 100644 --- a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts +++ b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts @@ -8,4 +8,5 @@ export enum FileExtensions { QnA = '.qna', lg = '.lg', Manifest = '.json', + BotProjectSpace = '.botproj', } diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 9416d09c0e..02a0b2cc9c 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -189,3 +189,16 @@ export interface BotInfo { diagnostics: Diagnostic[]; name: string; } + +export interface BotProjectSpaceSkill { + workspace: string; + manifest: string; + remote: boolean; + endpointName: string; +} + +export interface BotProjectSpace { + workspace: string; + name: string; + skills: BotProjectSpaceSkill[]; +} diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index abc5938af2..a1da5925ae 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -681,6 +681,7 @@ export class BotProject implements IBotProject { 'sdk.override.uischema', 'sdk.schema', 'sdk.uischema', + '*.botproj', ]; for (const pattern of patterns) { // load only from the data dir, otherwise may get "build" versions from From c64ecf209a3cf593eed017b4212e29ae45e2c291 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sun, 20 Sep 2020 20:17:28 -0700 Subject: [PATCH 005/100] Backup Signed-off-by: Srinaath Ravichandran creat new bot Signed-off-by: Srinaath Ravichandran Bot project file Signed-off-by: Srinaath Ravichandran --- Composer/package.json | 2 +- .../client/src/Onboarding/Onboarding.tsx | 9 +- .../components/CreationFlow/CreationFlow.tsx | 15 +- .../src/pages/knowledge-base/QnAPage.tsx | 1 + .../src/recoilModel/DispatcherWrapper.tsx | 32 +- .../client/src/recoilModel/atoms/appState.ts | 9 +- .../client/src/recoilModel/atoms/botState.ts | 21 +- .../dispatchers/__tests__/project.test.tsx | 1 - .../recoilModel/dispatchers/botProjectFile.ts | 92 ++++ .../src/recoilModel/dispatchers/index.ts | 2 + .../recoilModel/dispatchers/project-backup.ts | 280 ------------ .../src/recoilModel/dispatchers/project.ts | 417 ++++-------------- .../src/recoilModel/dispatchers/publisher.ts | 18 +- .../src/recoilModel/dispatchers/storage.ts | 55 ++- .../recoilModel/dispatchers/utils/project.ts | 369 ++++++++++++++-- .../src/recoilModel/selectors/design.ts | 4 +- .../src/recoilModel/selectors/project.ts | 78 ++++ .../undo/__test__/history.test.tsx | 6 +- .../client/src/recoilModel/undo/history.ts | 11 +- Composer/packages/client/src/router.tsx | 14 +- .../packages/client/src/utils/fileUtil.ts | 8 + .../packages/lib/shared/src/types/indexers.ts | 6 +- .../server/src/controllers/project.ts | 33 +- Composer/packages/server/src/router/api.ts | 1 + 24 files changed, 776 insertions(+), 708 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts delete mode 100644 Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts create mode 100644 Composer/packages/client/src/recoilModel/selectors/project.ts diff --git a/Composer/package.json b/Composer/package.json index 01e987223e..4f5d112940 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "build:plugins:runtimes": "cd plugins/runtimes && yarn install && yarn build", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", "startall": "yarn start", - "start:dev": "concurrently \"npm:start:client\"", + "start:dev": "concurrently \"npm:start:client\" \"npm:start:server\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", "start:server:dev": "yarn workspace @bfc/server start:dev", diff --git a/Composer/packages/client/src/Onboarding/Onboarding.tsx b/Composer/packages/client/src/Onboarding/Onboarding.tsx index 81b5c2b20b..13ebabe2be 100644 --- a/Composer/packages/client/src/Onboarding/Onboarding.tsx +++ b/Composer/packages/client/src/Onboarding/Onboarding.tsx @@ -9,7 +9,12 @@ import { useRecoilValue } from 'recoil'; import onboardingStorage from '../utils/onboardingStorage'; import { OpenConfirmModal } from '../components/Modal/ConfirmDialog'; import { useLocation } from '../utils/hooks'; -import { dispatcherState, onboardingState, botProjectsSpaceState, validateDialogSelectorFamily } from '../recoilModel'; +import { + dispatcherState, + onboardingState, + botProjectSpaceProjectIds, + validateDialogSelectorFamily, +} from '../recoilModel'; import OnboardingContext from './OnboardingContext'; import TeachingBubbles from './TeachingBubbles/TeachingBubbles'; @@ -20,7 +25,7 @@ const getCurrentSet = (stepSets) => stepSets.findIndex(({ id }) => id === onboar const Onboarding: React.FC = () => { const didMount = useRef(false); - const botProjects = useRecoilValue(botProjectsSpaceState); + const botProjects = useRecoilValue(botProjectSpaceProjectIds); const rootBotProjectId = botProjects[0]; const dialogs = useRecoilValue(validateDialogSelectorFamily(rootBotProjectId)); const { onboardingSetComplete } = useRecoilValue(dispatcherState); diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index bcfff92fc8..3c01b9b992 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -22,6 +22,7 @@ import Home from '../../pages/home/Home'; import ImportQnAFromUrlModal from '../../pages/knowledge-base/ImportQnAFromUrlModal'; import { QnABotTemplateId } from '../../constants'; import { useProjectIdCache } from '../../utils/hooks'; +import { projectLoadSelector } from '../../recoilModel/selectors/project'; import { CreateOptions } from './CreateOptions'; import { OpenProject } from './OpenProject'; @@ -32,9 +33,6 @@ type CreationFlowProps = RouteComponentProps<{}>; const CreationFlow: React.FC = () => { const { fetchTemplates, - openProject, - createProject, - saveProjectAs, fetchStorages, fetchFolderItemsByPath, setCreationFlowStatus, @@ -42,9 +40,10 @@ const CreationFlow: React.FC = () => { updateCurrentPathForStorage, updateFolder, saveTemplateId, - fetchProjectById, fetchRecentProjects, } = useRecoilValue(dispatcherState); + + const projectLoader = useRecoilValue(projectLoadSelector); const creationFlowStatus = useRecoilValue(creationFlowStatusState); const projectId = useRecoilValue(currentProjectIdState); const templateProjects = useRecoilValue(templateProjectsState); @@ -70,7 +69,7 @@ const CreationFlow: React.FC = () => { // fetchProject use `gotoSnapshot` which will wipe out all state value. // so here make those methods call in sequence. if (!projectId && cachedProjectId) { - await fetchProjectById(cachedProjectId); + await projectLoader.fetchProjectById(cachedProjectId); } await fetchStorages(); fetchTemplates(); @@ -98,11 +97,11 @@ const CreationFlow: React.FC = () => { const openBot = async (botFolder) => { setCreationFlowStatus(CreationFlowStatus.CLOSE); - openProject(botFolder); + projectLoader.openProject(botFolder); }; const handleCreateNew = async (formData, templateId: string, qnaKbUrls?: string[]) => { - createProject( + projectLoader.createProject( templateId || '', formData.name, formData.description, @@ -114,7 +113,7 @@ const CreationFlow: React.FC = () => { }; const handleSaveAs = (formData) => { - saveProjectAs(projectId, formData.name, formData.description, formData.location); + projectLoader.saveProjectAs(projectId, formData.name, formData.description, formData.location); }; const handleCreateQnA = async (urls: string[]) => { diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index c4b90d6390..5e38f9b824 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -31,6 +31,7 @@ interface QnAPageProps extends RouteComponentProps<{}> { const QnAPage: React.FC = (props) => { const { dialogId = '', projectId = '' } = props; + const actions = useRecoilValue(dispatcherState); const dialogs = useRecoilValue(dialogsState(projectId)); const botName = useRecoilValue(botNameState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 055ec170e4..e5aaf1c48b 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -14,7 +14,6 @@ import { UndoRoot } from './undo/history'; import { prepareAxios } from './../utils/auth'; import createDispatchers, { Dispatcher } from './dispatchers'; import { - botProjectsSpaceState, dialogsState, luFilesState, qnaFilesState, @@ -23,7 +22,10 @@ import { dialogSchemasState, settingsState, filePersistenceState, + projectMetaDataState, + botProjectSpaceLoadedState, } from './atoms'; +import { projectMetaDataSelector } from './selectors/project'; const getBotAssets = async (projectId, snapshot: Snapshot): Promise => { const result = await Promise.all([ @@ -83,24 +85,32 @@ const InitDispatcher = ({ onLoad }) => { export const DispatcherWrapper = ({ children }) => { const [loaded, setLoaded] = useState(false); - const botProjects = useRecoilValue(botProjectsSpaceState); + const botProjects = useRecoilValue(projectMetaDataSelector); + const botProjectLoaded = useRecoilValue(botProjectSpaceLoadedState); useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => { - for (const projectId of botProjects) { - const assets = await getBotAssets(projectId, snapshot); - const previousAssets = await getBotAssets(projectId, previousSnapshot); - const filePersistence = await snapshot.getPromise(filePersistenceState(projectId)); - if (!isEmpty(filePersistence)) { - filePersistence.notify(assets, previousAssets); + if (botProjectLoaded) { + for (const { projectId } of botProjects) { + const metaData = await snapshot.getPromise(projectMetaDataState(projectId)); + if (!metaData.isRemote) { + const assets = await getBotAssets(projectId, snapshot); + const previousAssets = await getBotAssets(projectId, previousSnapshot); + const filePersistence = await snapshot.getPromise(filePersistenceState(projectId)); + if (!isEmpty(filePersistence)) { + filePersistence.notify(assets, previousAssets); + } + } } } }); return ( - {botProjects.map((projectId) => ( - - ))} + {botProjects + .filter(({ isRemote }) => !isRemote) + .map(({ projectId }) => ( + + ))} {loaded ? children : null} diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 06179b9134..6b73658839 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -157,7 +157,7 @@ export const extensionsState = atom({ default: [], }); -export const botProjectsSpaceState = atom({ +export const botProjectSpaceProjectIds = atom({ key: getFullyQualifiedKey('botProjectsSpace'), default: [], }); @@ -167,7 +167,12 @@ export const currentProjectIdState = atom({ default: '', }); +export const botProjectSpaceLoadedState = atom({ + key: getFullyQualifiedKey('botProjectSpaceLoadedState'), + default: false, +}); + export const botOpeningState = atom({ - key: getFullyQualifiedKey('botOpening'), + key: getFullyQualifiedKey('botOpeningState'), default: false, }); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 5880e99a3d..e9b75d2ad2 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -12,6 +12,7 @@ import { BotSchemas, Skill, DialogSetting, + BotProjectSpace, } from '@bfc/shared'; import { BotLoadError, DesignPageLocation, QnAAllUpViewStatus } from '../../recoilModel/types'; @@ -218,10 +219,13 @@ export const onDelLanguageDialogCompleteState = atomFamily({ default: { func: undefined }, }); -export const projectMetaDataState = atomFamily({ +export const projectMetaDataState = atomFamily<{ isRootBot: boolean; isRemote: boolean }, string>({ key: getFullyQualifiedKey('projectsMetaDataState'), - default: (id) => { - return {}; + default: () => { + return { + isRootBot: false, + isRemote: false, + }; }, }); @@ -254,3 +258,14 @@ export const filePersistenceState = atomFamily({ default: {} as FilePersistence, dangerouslyAllowMutability: true, }); + +export const botProjectFileState = atomFamily({ + key: getFullyQualifiedKey('botProjectFileState'), + default: {} as BotProjectSpace, +}); + +// Always using the first manifest file for now. We dont have the need to support multiple manifest for a skill +export const currentManifestIndex = atomFamily({ + key: getFullyQualifiedKey('currentManifestIndexState'), + default: 0, +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index b572b7f35d..e48af4afe7 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -28,7 +28,6 @@ import { schemasState, locationState, skillsState, - botOpeningState, botStatusState, botNameState, } from '../../atoms'; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts new file mode 100644 index 0000000000..88343ed57f --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -0,0 +1,92 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CallbackInterface, useRecoilCallback } from 'recoil'; +import { produce } from 'immer'; +import { BotProjectSpace, BotProjectSpaceSkill } from '@bfc/shared'; + +import { botNameState, botProjectFileState, locationState, skillManifestsState } from '../atoms'; +import { convertPathToFileProtocol, trimFileProtocol } from '../../utils/fileUtil'; + +export const botProjectFileDispatcher = () => { + const addSkillToBotProject = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + const skillLocation = await snapshot.getPromise(locationState(projectId)); + const manifests: { id: string; content: string; lastModified: string }[] = await snapshot.getPromise( + skillManifestsState(projectId) + ); + // TODO:// We would support only 1 manifest per skill. It will always be the first manifest. We would need UI in future to set the default manifest file + const currentManifest = manifests[0]; + + set(botProjectFileState(projectId), (current: BotProjectSpace) => { + const result = produce(current, (draftState: BotProjectSpace) => { + const skill: BotProjectSpaceSkill = { + manifest: currentManifest.id, + workspace: convertPathToFileProtocol(skillLocation), + remote: false, + }; + draftState.skills.push(skill); + }); + return result; + }); + } + ); + + const removeLocalSkillFromBotProject = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + const skillLocation = await snapshot.getPromise(locationState(projectId)); + snapshot.getPromise(skillManifestsState(projectId)); + + set(botProjectFileState(projectId), (current: BotProjectSpace) => { + const result = produce(current, (draftState: BotProjectSpace) => { + draftState.skills = draftState.skills.filter(({ workspace }) => { + if (workspace) { + return trimFileProtocol(workspace) !== skillLocation; + } + return true; + }); + }); + return result; + }); + } + ); + + const removeRemoteSkillFromBotProject = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + const manifestLocation = await snapshot.getPromise(locationState(projectId)); + set(botProjectFileState(projectId), (current: BotProjectSpace) => { + const result = produce(current, (draftState: BotProjectSpace) => { + draftState.skills = draftState.skills.filter(({ manifest, remote }) => { + if (remote) { + return manifestLocation !== manifest; + } + return true; + }); + }); + return result; + }); + } + ); + + const renameRootBotFromBotProject = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + const location = await snapshot.getPromise(locationState(projectId)); + const botname = await snapshot.getPromise(botNameState(projectId)); + set(botProjectFileState(projectId), (current: BotProjectSpace) => { + const result = produce(current, (draftState: BotProjectSpace) => { + draftState.workspace = convertPathToFileProtocol(location); + draftState.name = botname; + }); + return result; + }); + } + ); + + return { + addSkillToBotProject, + removeLocalSkillFromBotProject, + removeRemoteSkillFromBotProject, + renameRootBotFromBotProject, + }; +}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/index.ts b/Composer/packages/client/src/recoilModel/dispatchers/index.ts index 983b536f4e..132c4d5c7a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/index.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/index.ts @@ -19,6 +19,7 @@ import { skillDispatcher } from './skill'; import { userDispatcher } from './user'; import { multilangDispatcher } from './multilang'; import { extensionsDispatcher } from './extensions'; +import { botProjectFileDispatcher } from './botProjectFile'; const createDispatchers = () => { return { @@ -40,6 +41,7 @@ const createDispatchers = () => { ...userDispatcher(), ...multilangDispatcher(), ...extensionsDispatcher(), + ...botProjectFileDispatcher(), }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts b/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts deleted file mode 100644 index 13e02dd619..0000000000 --- a/Composer/packages/client/src/recoilModel/dispatchers/project-backup.ts +++ /dev/null @@ -1,280 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import path from 'path'; - -import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { - dereferenceDefinitions, - LuFile, - QnAFile, - DialogInfo, - SensitiveProperties, - DialogSetting, - convertSkillsToDictionary, -} from '@bfc/shared'; -import queryString from 'query-string'; -import { indexer, validateDialog } from '@bfc/indexers'; -import objectGet from 'lodash/get'; -import objectSet from 'lodash/set'; -import formatMessage from 'format-message'; - -import lgWorker from '../parsers/lgWorker'; -import luWorker from '../parsers/luWorker'; -import qnaWorker from '../parsers/qnaWorker'; -import httpClient from '../../utils/httpUtil'; -import { BotStatus } from '../../constants'; -import { getReferredLuFiles } from '../../utils/luUtil'; -import luFileStatusStorage from '../../utils/luFileStatusStorage'; -import { getReferredQnaFiles } from '../../utils/qnaUtil'; -import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; -import settingStorage from '../../utils/dialogSettingStorage'; -import filePersistence from '../persistence/FilePersistence'; -import { navigateTo } from '../../utils/navigation'; -import languageStorage from '../../utils/languageStorage'; -import { projectIdCache } from '../../utils/projectCache'; -import { - designPageLocationState, - botDiagnosticsState, - botProjectsSpaceState, - projectMetaDataState, - filePersistenceState, - currentProjectIdState, - botOpeningState, -} from '../atoms'; -import { QnABotTemplateId } from '../../constants'; -import FilePersistence from '../persistence/FilePersistence'; -import UndoHistory from '../undo/undoHistory'; -import { undoHistoryState } from '../undo/history'; - -import { - skillManifestsState, - settingsState, - localeState, - luFilesState, - qnaFilesState, - skillsState, - schemasState, - lgFilesState, - locationState, - botStatusState, - botNameState, - botEnvironmentState, - dialogsState, - recentProjectsState, - templateProjectsState, - runtimeTemplatesState, - applicationErrorState, - templateIdState, - announcementState, - boilerplateVersionState, - dialogSchemasState, -} from './../atoms'; -import { logMessage, setError } from './../dispatchers/shared'; -import { - fetchProjectDataByPath, - fetchProjectDataById, - flushExistingTasks, - getMergedSettings, - navigateToBot, - parseSkillPaths, -} from './utils/project'; - -const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { - callbackHelpers.set(botOpeningState, false); - setError(callbackHelpers, ex); -}; - -const removeRecentProject = async (callbackHelpers: CallbackInterface, path: string) => { - try { - const { - set, - snapshot: { getPromise }, - } = callbackHelpers; - const currentRecentProjects = await getPromise(recentProjectsState); - const filtered = currentRecentProjects.filter((p) => p.path !== path); - set(recentProjectsState, filtered); - } catch (ex) { - logMessage(callbackHelpers, `Error removing recent project: ${ex}`); - } -}; - -const processSchema = (projectId: string, schema: any) => ({ - ...schema, - definitions: dereferenceDefinitions(schema.definitions), -}); - -// if user set value in terminal or appsetting.json, it should update the value in localStorage -const refreshLocalStorage = (projectId: string, settings: DialogSetting) => { - for (const property of SensitiveProperties) { - const value = objectGet(settings, property); - if (value) { - settingStorage.setField(projectId, property, value); - } - } -}; - -// merge sensitive values in localStorage -const mergeLocalStorage = (projectId: string, settings: DialogSetting) => { - const localSetting = settingStorage.get(projectId); - const mergedSettings = { ...settings }; - if (localSetting) { - for (const property of SensitiveProperties) { - const value = objectGet(localSetting, property); - if (value) { - objectSet(mergedSettings, property, value); - } else { - objectSet(mergedSettings, property, ''); // set those key back, because that were omit after persisited - } - } - } - return mergedSettings; -}; - -const updateLuFilesStatus = (projectId: string, luFiles: LuFile[]) => { - const status = luFileStatusStorage.get(projectId); - return luFiles.map((luFile) => { - if (typeof status[luFile.id] === 'boolean') { - return { ...luFile, published: status[luFile.id] }; - } else { - return { ...luFile, published: false }; - } - }); -}; - -const initLuFilesStatus = (projectId: string, luFiles: LuFile[], dialogs: DialogInfo[]) => { - luFileStatusStorage.checkFileStatus( - projectId, - getReferredLuFiles(luFiles, dialogs).map((file) => file.id) - ); - return updateLuFilesStatus(projectId, luFiles); -}; - -const updateQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[]) => { - const status = qnaFileStatusStorage.get(projectId); - return qnaFiles.map((qnaFile) => { - if (typeof status[qnaFile.id] === 'boolean') { - return { ...qnaFile, published: status[qnaFile.id] }; - } else { - return { ...qnaFile, published: false }; - } - }); -}; - -const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: DialogInfo[]) => { - qnaFileStatusStorage.checkFileStatus( - projectId, - getReferredQnaFiles(qnaFiles, dialogs).map((file) => file.id) - ); - return updateQnaFilesStatus(projectId, qnaFiles); -}; - -export const projectDispatcher = () => { - const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { - const { snapshot, set } = callbackHelpers; - const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; - const curLocation = await snapshot.getPromise(locationState(projectId)); - const storedLocale = languageStorage.get(botName)?.locale; - const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; - - // cache current projectId in session, resolve page refresh caused state lost. - projectIdCache.set(projectId); - - try { - schemas.sdk.content = processSchema(projectId, schemas.sdk.content); - } catch (err) { - const diagnostics = schemas.diagnostics ?? []; - diagnostics.push(err.message); - schemas.diagnostics = diagnostics; - } - - try { - let mainDialog = ''; - const verifiedDialogs = dialogs.map((dialog) => { - if (dialog.isRoot) { - mainDialog = dialog.id; - } - dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); - return dialog; - }); - - await lgWorker.addProject(projectId, lgFiles); - set(botProjectsSpaceState, []); - - set(skillManifestsState(projectId), skillManifestFiles); - set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); - set(lgFilesState(projectId), lgFiles); - set(dialogsState(projectId), verifiedDialogs); - set(dialogSchemasState(projectId), dialogSchemas); - set(botEnvironmentState(projectId), botEnvironment); - set(botNameState(projectId), botName); - set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); - if (location !== curLocation) { - set(botStatusState(projectId), BotStatus.unConnected); - set(locationState(projectId), location); - } - set(skillsState(projectId), skills); - set(schemasState(projectId), schemas); - set(localeState(projectId), locale); - set(botDiagnosticsState(projectId), diagnostics); - - refreshLocalStorage(projectId, settings); - set(settingsState(projectId), mergedSettings); - set(filePersistenceState(projectId), new FilePersistence(projectId)); - set(undoHistoryState(projectId), new UndoHistory(projectId)); - set(projectMetaDataState(projectId), { - isRootBot, - }); - return mainDialog; - } catch (err) { - callbackHelpers.set(botOpeningState, false); - setError(callbackHelpers, err); - navigateTo('/home'); - return ''; - } - }; - - const openSkill = async (callbackHelpers, skill: { path: string; remote: boolean }, storageId) => { - const { set } = callbackHelpers; - try { - if (!skill.remote) { - const { projectData, botFiles } = await fetchProjectDataByPath(skill.path, storageId); - await initBotState(callbackHelpers, projectData, botFiles); - set(botProjectsSpaceState, (current) => [...current, projectData.projectId]); - } - } catch (ex) { - // Handle exception in opening a skill - } - }; - - const openProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { - try { - const { set } = callbackHelpers; - await flushExistingTasks(callbackHelpers); - const { projectData, botFiles } = await fetchProjectDataByPath(path, storageId); - const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); - set(botProjectsSpaceState, []); - if (botFiles.botProjectSpaceFiles.length) { - // Handle botproject space code here. CUrrently always fetching the first BotProject file received. In future, there would be a file for each environment - const skillsInBotProject = parseSkillPaths(botFiles.botProjectSpaceFiles[0]); - skillsInBotProject.forEach((skillInBotProject) => { - openSkill(callbackHelpers, skillInBotProject, storageId); - }); - } - //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject - set(botProjectsSpaceState, [projectData.projectId]); - set(currentProjectIdState, projectData.projectId); - navigateToBot(projectData.projectId, mainDialog); - } catch (ex) { - removeRecentProject(callbackHelpers, path); - handleProjectFailure(callbackHelpers, ex); - } - } - ); - - return { - openProject, - }; -}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index e5eb6a3c03..b0c2022301 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -1,288 +1,104 @@ /* eslint-disable react-hooks/rules-of-hooks */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import path from 'path'; import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { - dereferenceDefinitions, - LuFile, - QnAFile, - DialogInfo, - SensitiveProperties, - DialogSetting, - convertSkillsToDictionary, -} from '@bfc/shared'; -import queryString from 'query-string'; -import { indexer, validateDialog } from '@bfc/indexers'; -import objectGet from 'lodash/get'; -import objectSet from 'lodash/set'; import formatMessage from 'format-message'; -import lgWorker from '../parsers/lgWorker'; -import luWorker from '../parsers/luWorker'; -import qnaWorker from '../parsers/qnaWorker'; import httpClient from '../../utils/httpUtil'; import { BotStatus } from '../../constants'; -import { getReferredLuFiles } from '../../utils/luUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; -import { getReferredQnaFiles } from '../../utils/qnaUtil'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import settingStorage from '../../utils/dialogSettingStorage'; -import filePersistence from '../persistence/FilePersistence'; import { navigateTo } from '../../utils/navigation'; -import languageStorage from '../../utils/languageStorage'; import { projectIdCache } from '../../utils/projectCache'; -import { - designPageLocationState, - botDiagnosticsState, - botProjectsSpaceState, - projectMetaDataState, - filePersistenceState, - currentProjectIdState, - botOpeningState, -} from '../atoms'; -import { QnABotTemplateId } from '../../constants'; -import FilePersistence from '../persistence/FilePersistence'; -import UndoHistory from '../undo/undoHistory'; -import { undoHistoryState } from '../undo/history'; +import { botProjectSpaceProjectIds, currentProjectIdState, botStatusState } from '../atoms'; -import { - skillManifestsState, - settingsState, - localeState, - luFilesState, - qnaFilesState, - skillsState, - schemasState, - lgFilesState, - locationState, - botStatusState, - botNameState, - botEnvironmentState, - dialogsState, - recentProjectsState, - templateProjectsState, - runtimeTemplatesState, - applicationErrorState, - templateIdState, - announcementState, - boilerplateVersionState, - dialogSchemasState, -} from './../atoms'; +import { recentProjectsState, templateIdState, announcementState, boilerplateVersionState } from './../atoms'; import { logMessage, setError } from './../dispatchers/shared'; import { fetchProjectDataById, fetchProjectDataByPath, flushExistingTasks, + handleProjectFailure, navigateToBot, - parseSkillPaths, + openLocalSkill, + initBotState, + removeRecentProject, + openRootBotAndSkills, + createNewBotFromTemplate, + resetBotStates, + openRemoteSkill, } from './utils/project'; -const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { - callbackHelpers.set(botOpeningState, false); - setError(callbackHelpers, ex); -}; - -const processSchema = (projectId: string, schema: any) => ({ - ...schema, - definitions: dereferenceDefinitions(schema.definitions), -}); - -// if user set value in terminal or appsetting.json, it should update the value in localStorage -const refreshLocalStorage = (projectId: string, settings: DialogSetting) => { - for (const property of SensitiveProperties) { - const value = objectGet(settings, property); - if (value) { - settingStorage.setField(projectId, property, value); - } - } -}; - -// merge sensitive values in localStorage -const mergeLocalStorage = (projectId: string, settings: DialogSetting) => { - const localSetting = settingStorage.get(projectId); - const mergedSettings = { ...settings }; - if (localSetting) { - for (const property of SensitiveProperties) { - const value = objectGet(localSetting, property); - if (value) { - objectSet(mergedSettings, property, value); - } else { - objectSet(mergedSettings, property, ''); // set those key back, because that were omit after persisited +export const projectDispatcher = () => { + const removeSkillFromBotProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (projectId: string) => { + try { + const { set } = callbackHelpers; + set(botProjectSpaceProjectIds, (currentProjects) => currentProjects.filter((id) => id !== projectId)); + } catch (ex) { + setError(callbackHelpers, ex); } } - } - return mergedSettings; -}; - -const updateLuFilesStatus = (projectId: string, luFiles: LuFile[]) => { - const status = luFileStatusStorage.get(projectId); - return luFiles.map((luFile) => { - if (typeof status[luFile.id] === 'boolean') { - return { ...luFile, published: status[luFile.id] }; - } else { - return { ...luFile, published: false }; - } - }); -}; - -const initLuFilesStatus = (projectId: string, luFiles: LuFile[], dialogs: DialogInfo[]) => { - luFileStatusStorage.checkFileStatus( - projectId, - getReferredLuFiles(luFiles, dialogs).map((file) => file.id) ); - return updateLuFilesStatus(projectId, luFiles); -}; -const updateQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[]) => { - const status = qnaFileStatusStorage.get(projectId); - return qnaFiles.map((qnaFile) => { - if (typeof status[qnaFile.id] === 'boolean') { - return { ...qnaFile, published: status[qnaFile.id] }; - } else { - return { ...qnaFile, published: false }; + const addExistingSkillToBotProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { + const { set } = callbackHelpers; + const projectId = await openLocalSkill(callbackHelpers, path, storageId); + set(botProjectSpaceProjectIds, (current) => [...current, projectId]); } - }); -}; - -const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: DialogInfo[]) => { - qnaFileStatusStorage.checkFileStatus( - projectId, - getReferredQnaFiles(qnaFiles, dialogs).map((file) => file.id) ); - return updateQnaFilesStatus(projectId, qnaFiles); -}; - -export const projectDispatcher = () => { - const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { - const { snapshot, set } = callbackHelpers; - const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; - const curLocation = await snapshot.getPromise(locationState(projectId)); - const storedLocale = languageStorage.get(botName)?.locale; - const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; - // cache current projectId in session, resolve page refresh caused state lost. - projectIdCache.set(projectId); - - try { - schemas.sdk.content = processSchema(projectId, schemas.sdk.content); - } catch (err) { - const diagnostics = schemas.diagnostics ?? []; - diagnostics.push(err.message); - schemas.diagnostics = diagnostics; - } - - try { - let mainDialog = ''; - const verifiedDialogs = dialogs.map((dialog) => { - if (dialog.isRoot) { - mainDialog = dialog.id; - } - dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); - return dialog; - }); - - await lgWorker.addProject(projectId, lgFiles); - - set(skillManifestsState(projectId), skillManifestFiles); - set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); - set(lgFilesState(projectId), lgFiles); - set(dialogsState(projectId), verifiedDialogs); - set(dialogSchemasState(projectId), dialogSchemas); - set(botEnvironmentState(projectId), botEnvironment); - set(botNameState(projectId), botName); - set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); - if (location !== curLocation) { - set(botStatusState(projectId), BotStatus.unConnected); - set(locationState(projectId), location); + const addRemoteSkillToBotProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { + try { + openRemoteSkill(callbackHelpers, manifestUrl, name, endpointName); + } catch (ex) { + // Handle exception } - set(skillsState(projectId), skills); - set(schemasState(projectId), schemas); - set(localeState(projectId), locale); - set(botDiagnosticsState(projectId), diagnostics); - - refreshLocalStorage(projectId, settings); - set(settingsState(projectId), mergedSettings); - set(filePersistenceState(projectId), new FilePersistence(projectId)); - set(undoHistoryState(projectId), new UndoHistory(projectId)); - set(projectMetaDataState(projectId), { - isRootBot, - }); - return mainDialog; - } catch (err) { - callbackHelpers.set(botOpeningState, false); - setError(callbackHelpers, err); - navigateTo('/home'); - return ''; - } - }; - - const removeRecentProject = async (callbackHelpers: CallbackInterface, path: string) => { - try { - const { - set, - snapshot: { getPromise }, - } = callbackHelpers; - const currentRecentProjects = await getPromise(recentProjectsState); - const filtered = currentRecentProjects.filter((p) => p.path !== path); - set(recentProjectsState, filtered); - } catch (ex) { - logMessage(callbackHelpers, `Error removing recent project: ${ex}`); } - }; - - const setBotOpeningStatus = async (callbackHelpers: CallbackInterface) => { - const { set, snapshot } = callbackHelpers; - set(botOpeningState, true); - const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState); - const filePersistenceHandlers: filePersistence[] = []; - for (const projectId of botProjectSpace) { - const fp = await snapshot.getPromise(filePersistenceState(projectId)); - filePersistenceHandlers.push(fp); - } - const workers = [lgWorker, luWorker, qnaWorker, ...filePersistenceHandlers]; - return Promise.all(workers.map((w) => w.flush())); - }; + ); - const openSkill = async (callbackHelpers, skill: { path: string; remote: boolean }, storageId) => { - const { set } = callbackHelpers; - try { - if (!skill.remote) { - const { projectData, botFiles } = await fetchProjectDataByPath(skill.path, storageId); - await initBotState(callbackHelpers, projectData, botFiles); - set(botProjectsSpaceState, (current) => [...current, projectData.id]); + const addNewSkillToBotProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async ( + templateId: string, + name: string, + description: string, + location: string, + schemaUrl?: string, + locale?: string, + qnaKbUrls?: string[] + ) => { + try { + const { set } = callbackHelpers; + const { projectId, mainDialog } = await createNewBotFromTemplate( + callbackHelpers, + templateId, + name, + description, + location, + schemaUrl, + locale + ); + set(botProjectSpaceProjectIds, (current) => [...current, projectId]); + navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); + } catch (ex) { + // Handle exception in opening a skill } - } catch (ex) { - // Handle exception in opening a skill } - }; - - const handleBotOpening = async (callbackHelpers, projectData, botFiles, storageId) => { - const { set } = callbackHelpers; - const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); - set(botProjectsSpaceState, []); - if (botFiles.botProjectSpaceFiles.length) { - // Handle botproject space code here. CUrrently always fetching the first BotProject file received. In future, there would be a file for each environment - const skillsInBotProject = parseSkillPaths(botFiles.botProjectSpaceFiles[0]); - skillsInBotProject.forEach((skillInBotProject) => { - openSkill(callbackHelpers, skillInBotProject, storageId); - }); - } - //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject - set(botProjectsSpaceState, [projectData.id]); - set(currentProjectIdState, projectData.id); - navigateToBot(projectData.id, mainDialog); - }; + ); const openProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { + const { set } = callbackHelpers; try { await flushExistingTasks(callbackHelpers); const { projectData, botFiles } = await fetchProjectDataByPath(path, storageId); - handleBotOpening(callbackHelpers, projectData, botFiles, storageId); + openRootBotAndSkills(callbackHelpers, projectData, botFiles, 'default'); } catch (ex) { + set(botProjectSpaceProjectIds, []); removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); } @@ -293,9 +109,9 @@ export const projectDispatcher = () => { try { const { set } = callbackHelpers; await flushExistingTasks(callbackHelpers); - set(botProjectsSpaceState, []); + set(botProjectSpaceProjectIds, []); const { projectData, botFiles } = await fetchProjectDataById(projectId); - handleBotOpening(callbackHelpers, projectData, botFiles, 'default'); + openRootBotAndSkills(callbackHelpers, projectData, botFiles, 'default'); } catch (ex) { handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); @@ -312,54 +128,39 @@ export const projectDispatcher = () => { locale?: string, qnaKbUrls?: string[] ) => { + const { set } = callbackHelpers; try { - await setBotOpeningStatus(callbackHelpers); - const response = await httpClient.post(`/projects`, { - storageId: 'default', + const { projectId, mainDialog } = await createNewBotFromTemplate( + callbackHelpers, templateId, name, description, location, schemaUrl, - locale, - }); - const projectId = response.data.id; - if (settingStorage.get(projectId)) { - settingStorage.remove(projectId); - } - await initBotState(callbackHelpers, response.data, true, templateId, qnaKbUrls); + locale + ); + projectIdCache.set(projectId); + set(botProjectSpaceProjectIds, [projectId]); + set(currentProjectIdState, projectId); + navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); return projectId; } catch (ex) { + set(botProjectSpaceProjectIds, []); + removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); + navigateTo('/home'); } } ); const deleteBotProject = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { - const { reset } = callbackHelpers; try { await httpClient.delete(`/projects/${projectId}`); luFileStatusStorage.removeAllStatuses(projectId); qnaFileStatusStorage.removeAllStatuses(projectId); settingStorage.remove(projectId); projectIdCache.clear(); - reset(dialogsState(projectId)); - reset(botEnvironmentState(projectId)); - reset(botNameState(projectId)); - reset(botStatusState(projectId)); - reset(locationState(projectId)); - reset(lgFilesState(projectId)); - reset(skillsState(projectId)); - reset(schemasState(projectId)); - reset(luFilesState(projectId)); - reset(settingsState(projectId)); - reset(localeState(projectId)); - reset(skillManifestsState(projectId)); - reset(designPageLocationState(projectId)); - reset(filePersistenceState(projectId)); - reset(undoHistoryState(projectId)); - reset(botProjectsSpaceState); - reset(currentProjectIdState); + resetBotStates(callbackHelpers, projectId); } catch (e) { logMessage(callbackHelpers, e.message); } @@ -368,14 +169,14 @@ export const projectDispatcher = () => { const saveProjectAs = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (projectId, name, description, location) => { try { - await setBotOpeningStatus(callbackHelpers); + await flushExistingTasks(callbackHelpers); const response = await httpClient.post(`/projects/${projectId}/project/saveAs`, { storageId: 'default', name, description, location, }); - await initBotState(callbackHelpers, response.data, true, ''); + await initBotState(callbackHelpers, response.data, true); return response.data.id; } catch (ex) { handleProjectFailure(callbackHelpers, ex); @@ -395,70 +196,12 @@ export const projectDispatcher = () => { } }); - const fetchRuntimeTemplates = useRecoilCallback<[], Promise>( - (callbackHelpers: CallbackInterface) => async () => { - const { set } = callbackHelpers; - try { - const response = await httpClient.get(`/runtime/templates`); - if (Array.isArray(response.data)) { - set(runtimeTemplatesState, [...response.data]); - } - } catch (ex) { - // TODO: Handle exceptions - logMessage(callbackHelpers, `Error fetching runtime templates: ${ex}`); - } - } - ); - - const fetchTemplates = useRecoilCallback<[], Promise>((callbackHelpers: CallbackInterface) => async () => { - try { - const response = await httpClient.get(`/assets/projectTemplates`); - - const data = response && response.data; - - if (data && Array.isArray(data) && data.length > 0) { - callbackHelpers.set(templateProjectsState, data); - } - } catch (err) { - // TODO: Handle exceptions - logMessage(callbackHelpers, `Error fetching runtime templates: ${err}`); - } - }); - const setBotStatus = useRecoilCallback<[BotStatus, string], void>( ({ set }: CallbackInterface) => (status: BotStatus, projectId: string) => { set(botStatusState(projectId), status); } ); - const createFolder = useRecoilCallback<[string, string], Promise>( - ({ set }: CallbackInterface) => async (path, name) => { - const storageId = 'default'; - try { - await httpClient.post(`/storages/folder`, { path, name, storageId }); - } catch (err) { - set(applicationErrorState, { - message: err.message, - summary: formatMessage('Create Folder Error'), - }); - } - } - ); - - const updateFolder = useRecoilCallback<[string, string, string], Promise>( - ({ set }: CallbackInterface) => async (path, oldName, newName) => { - const storageId = 'default'; - try { - await httpClient.put(`/storages/folder`, { path, oldName, newName, storageId }); - } catch (err) { - set(applicationErrorState, { - message: err.message, - summary: formatMessage('Update Folder Name Error'), - }); - } - } - ); - const saveTemplateId = useRecoilCallback<[string], void>(({ set }: CallbackInterface) => (templateId) => { if (templateId) { set(templateIdState, templateId); @@ -489,15 +232,15 @@ export const projectDispatcher = () => { createProject, deleteBotProject, saveProjectAs, - fetchTemplates, fetchProjectById, fetchRecentProjects, - fetchRuntimeTemplates, setBotStatus, - updateFolder, - createFolder, saveTemplateId, updateBoilerplate, getBoilerplateVersion, + removeSkillFromBotProject, + addNewSkillToBotProject, + addExistingSkillToBotProject, + addRemoteSkillToBotProject, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index c9eeac9e33..15b98f62d1 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -14,7 +14,7 @@ import { isEjectRuntimeExistState, filePersistenceState, } from '../atoms/botState'; -import { botEndpointsState } from '../atoms'; +import { botEndpointsState, runtimeTemplatesState } from '../atoms'; import { BotStatus, Text } from './../../constants'; import httpClient from './../../utils/httpUtil'; @@ -217,6 +217,21 @@ export const publisherDispatcher = () => { } } ); + + const fetchRuntimeTemplates = useRecoilCallback<[], Promise>( + (callbackHelpers: CallbackInterface) => async () => { + const { set } = callbackHelpers; + try { + const response = await httpClient.get(`/runtime/templates`); + if (Array.isArray(response.data)) { + set(runtimeTemplatesState, [...response.data]); + } + } catch (ex) { + // TODO: Handle exceptions + logMessage(callbackHelpers, `Error fetching runtime templates: ${ex}`); + } + } + ); return { getPublishTargetTypes, publishToTarget, @@ -225,5 +240,6 @@ export const publisherDispatcher = () => { getPublishStatus, getPublishHistory, setEjectRuntimeExist, + fetchRuntimeTemplates, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/storage.ts b/Composer/packages/client/src/recoilModel/dispatchers/storage.ts index 6991a6b1f7..84f979f381 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/storage.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/storage.ts @@ -3,9 +3,16 @@ // Licensed under the MIT License. import { useRecoilCallback, CallbackInterface } from 'recoil'; import isArray from 'lodash/isArray'; +import formatMessage from 'format-message'; import httpClient from '../../utils/httpUtil'; -import { storagesState, storageFileLoadingStatusState, focusedStorageFolderState } from '../atoms/appState'; +import { + storagesState, + storageFileLoadingStatusState, + focusedStorageFolderState, + applicationErrorState, + templateProjectsState, +} from '../atoms/appState'; import { FileTypes } from '../../constants'; import { getExtension } from '../../utils/fileUtil'; @@ -101,6 +108,49 @@ export const storageDispatcher = () => { } ); + const createFolder = useRecoilCallback<[string, string], Promise>( + ({ set }: CallbackInterface) => async (path, name) => { + const storageId = 'default'; + try { + await httpClient.post(`/storages/folder`, { path, name, storageId }); + } catch (err) { + set(applicationErrorState, { + message: err.message, + summary: formatMessage('Create Folder Error'), + }); + } + } + ); + + const updateFolder = useRecoilCallback<[string, string, string], Promise>( + ({ set }: CallbackInterface) => async (path, oldName, newName) => { + const storageId = 'default'; + try { + await httpClient.put(`/storages/folder`, { path, oldName, newName, storageId }); + } catch (err) { + set(applicationErrorState, { + message: err.message, + summary: formatMessage('Update Folder Name Error'), + }); + } + } + ); + + const fetchTemplates = useRecoilCallback<[], Promise>((callbackHelpers: CallbackInterface) => async () => { + try { + const response = await httpClient.get(`/assets/projectTemplates`); + + const data = response && response.data; + + if (data && Array.isArray(data) && data.length > 0) { + callbackHelpers.set(templateProjectsState, data); + } + } catch (err) { + // TODO: Handle exceptions + logMessage(callbackHelpers, `Error fetching runtime templates: ${err}`); + } + }); + return { fetchStorages, updateCurrentPathForStorage, @@ -108,5 +158,8 @@ export const storageDispatcher = () => { fetchStorageByName, fetchFolderItemsByPath, setStorageFileLoadingStatus, + createFolder, + updateFolder, + fetchTemplates, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index eb9458e71a..c10034af05 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -3,33 +3,91 @@ import path from 'path'; -import { SensitiveProperties, convertSkillsToDictionary, DialogSetting, BotProjectSpaceSkill } from '@bfc/shared'; +import { + SensitiveProperties, + convertSkillsToDictionary, + DialogSetting, + dereferenceDefinitions, + DialogInfo, + LuFile, + BotProjectSpace, + QnAFile, +} from '@bfc/shared'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; -import { BotProjectSpace } from '@bfc/shared/src/types'; -import { indexer } from '@bfc/indexers'; +import { indexer, validateDialog } from '@bfc/indexers'; +import { CallbackInterface } from 'recoil'; +import { stringify } from 'query-string'; +import * as botstates from '../../atoms/botState'; +import UndoHistory from '../../undo/undoHistory'; import languageStorage from '../../../utils/languageStorage'; import settingStorage from '../../../utils/dialogSettingStorage'; -import { botProjectsSpaceState, filePersistenceState } from '../../atoms'; +import { + botDiagnosticsState, + botProjectFileState, + botProjectSpaceLoadedState, + botProjectSpaceProjectIds, + currentProjectIdState, + filePersistenceState, + projectMetaDataState, + qnaFilesState, + recentProjectsState, + botOpeningState, +} from '../../atoms'; import lgWorker from '../../parsers/lgWorker'; import luWorker from '../../parsers/luWorker'; import qnaWorker from '../../parsers/qnaWorker'; import FilePersistence from '../../persistence/FilePersistence'; import { navigateTo } from '../../../utils/navigation'; -import { QnABotTemplateId } from '../../../constants'; +import { BotStatus, QnABotTemplateId } from '../../../constants'; import httpClient from '../../../utils/httpUtil'; +import { getReferredLuFiles } from '../../../utils/luUtil'; +import luFileStatusStorage from '../../../utils/luFileStatusStorage'; +import { getReferredQnaFiles } from '../../../utils/qnaUtil'; +import qnaFileStatusStorage from '../../../utils/qnaFileStatusStorage'; +import { logMessage, setError } from '../shared'; +import { + skillManifestsState, + settingsState, + localeState, + luFilesState, + skillsState, + schemasState, + lgFilesState, + locationState, + botStatusState, + botNameState, + botEnvironmentState, + dialogsState, + dialogSchemasState, +} from '../../atoms'; +import { undoHistoryState } from '../../undo/history'; +import { dispatcherState } from '../../DispatcherWrapper'; + +export const resetBotStates = async ({ snapshot, gotoSnapshot }: CallbackInterface, projectId: string) => { + const botStates = Object.keys(botstates); + const newSnapshot = snapshot.map(({ reset }) => { + botStates.forEach((state) => { + const currentRecoilAtom: any = botstates[state]; + reset(currentRecoilAtom(projectId)); + }); + }); + gotoSnapshot(newSnapshot); +}; export const flushExistingTasks = async (callbackHelpers) => { - const { snapshot } = callbackHelpers; - const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState); - const filePersistenceHandlers: FilePersistence[] = []; - for (const projectId of botProjectSpace) { - const fp = await snapshot.getPromise(filePersistenceState(projectId)); - filePersistenceHandlers.push(fp); + const { snapshot, reset } = callbackHelpers; + const projectIds = await snapshot.getPromise(botProjectSpaceProjectIds); + const recoilTasks: any[] = []; + for (const projectId of projectIds) { + const resetStates = await resetBotStates(callbackHelpers, projectId); + recoilTasks.push(resetStates); } - const workers = [lgWorker, luWorker, qnaWorker, ...filePersistenceHandlers]; - return Promise.all(workers.map((w) => w.flush())); + reset(botProjectSpaceProjectIds); + const workers = [lgWorker, luWorker, qnaWorker]; + + return Promise.all([workers.map((w) => w.flush()), ...recoilTasks]); }; // merge sensitive values in localStorage @@ -114,24 +172,275 @@ export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; } }; -export const parseSkillPaths = (botProjectSpace: BotProjectSpace): { path: string; remote: boolean }[] => { - const rootBotPath = botProjectSpace.workspace.replace('file://', ''); - const result = botProjectSpace.skills.map((skill: BotProjectSpaceSkill) => { - if (skill.workspace) { - const { protocol } = new URL(skill.workspace); - if (protocol === 'file:') { - const relativeSkillPath = skill.workspace.replace('file://', ''); - const skillPath = path.resolve(rootBotPath, relativeSkillPath); - return { - path: path.normalize(skillPath), - remote: false, - }; - } - } +export const createNewProject = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { + try { + const response = await httpClient.get(`/projects/${projectId}`); + const projectData = loadProjectData(response); + return projectData; + } catch (ex) { return { - path: skill.manifest, - remote: false, + botFiles: undefined, + projectData: undefined, + error: ex, }; + } +}; + +export const parseFileProtocolPaths = (rootPath: string, relativePath): string => { + try { + const rootPathWithoutProtocol = rootPath.replace('file://', ''); + let skillPath = relativePath.replace('file://', ''); + if (skillPath) { + skillPath = path.resolve(rootPathWithoutProtocol, skillPath); + } + return path.normalize(skillPath); + } catch (ex) { + throw new Error('Invalid path'); + } +}; + +export const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { + callbackHelpers.set(botProjectSpaceLoadedState, false); + setError(callbackHelpers, ex); +}; + +export const processSchema = (projectId: string, schema: any) => ({ + ...schema, + definitions: dereferenceDefinitions(schema.definitions), +}); + +// if user set value in terminal or appsetting.json, it should update the value in localStorage +export const refreshLocalStorage = (projectId: string, settings: DialogSetting) => { + for (const property of SensitiveProperties) { + const value = objectGet(settings, property); + if (value) { + settingStorage.setField(projectId, property, value); + } + } +}; + +export const updateLuFilesStatus = (projectId: string, luFiles: LuFile[]) => { + const status = luFileStatusStorage.get(projectId); + return luFiles.map((luFile) => { + if (typeof status[luFile.id] === 'boolean') { + return { ...luFile, published: status[luFile.id] }; + } else { + return { ...luFile, published: false }; + } }); - return result; +}; + +export const initLuFilesStatus = (projectId: string, luFiles: LuFile[], dialogs: DialogInfo[]) => { + luFileStatusStorage.checkFileStatus( + projectId, + getReferredLuFiles(luFiles, dialogs).map((file) => file.id) + ); + return updateLuFilesStatus(projectId, luFiles); +}; + +export const updateQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[]) => { + const status = qnaFileStatusStorage.get(projectId); + return qnaFiles.map((qnaFile) => { + if (typeof status[qnaFile.id] === 'boolean') { + return { ...qnaFile, published: status[qnaFile.id] }; + } else { + return { ...qnaFile, published: false }; + } + }); +}; + +export const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: DialogInfo[]) => { + qnaFileStatusStorage.checkFileStatus( + projectId, + getReferredQnaFiles(qnaFiles, dialogs).map((file) => file.id) + ); + return updateQnaFilesStatus(projectId, qnaFiles); +}; + +export const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { + const { snapshot, set } = callbackHelpers; + const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; + const curLocation = await snapshot.getPromise(locationState(projectId)); + const storedLocale = languageStorage.get(botName)?.locale; + const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; + + try { + schemas.sdk.content = processSchema(projectId, schemas.sdk.content); + } catch (err) { + const diagnostics = schemas.diagnostics ?? []; + diagnostics.push(err.message); + schemas.diagnostics = diagnostics; + } + + let mainDialog = ''; + const verifiedDialogs = dialogs.map((dialog) => { + if (dialog.isRoot) { + mainDialog = dialog.id; + } + dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles); + return dialog; + }); + + await lgWorker.addProject(projectId, lgFiles); + + set(skillManifestsState(projectId), skillManifestFiles); + set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); + set(lgFilesState(projectId), lgFiles); + set(dialogsState(projectId), verifiedDialogs); + set(dialogSchemasState(projectId), dialogSchemas); + set(botEnvironmentState(projectId), botEnvironment); + set(botNameState(projectId), botName); + set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); + if (location !== curLocation) { + set(botStatusState(projectId), BotStatus.unConnected); + set(locationState(projectId), location); + } + set(skillsState(projectId), skills); + set(schemasState(projectId), schemas); + set(localeState(projectId), locale); + set(botDiagnosticsState(projectId), diagnostics); + + refreshLocalStorage(projectId, settings); + set(settingsState(projectId), mergedSettings); + set(filePersistenceState(projectId), new FilePersistence(projectId)); + set(undoHistoryState(projectId), new UndoHistory(projectId)); + set(projectMetaDataState(projectId), { + isRootBot, + }); + return mainDialog; +}; + +export const removeRecentProject = async (callbackHelpers: CallbackInterface, path: string) => { + try { + const { + set, + snapshot: { getPromise }, + } = callbackHelpers; + const currentRecentProjects = await getPromise(recentProjectsState); + const filtered = currentRecentProjects.filter((p) => p.path !== path); + set(recentProjectsState, filtered); + } catch (ex) { + logMessage(callbackHelpers, `Error removing recent project: ${ex}`); + } +}; + +export const openRemoteSkill = async ( + callbackHelpers: CallbackInterface, + manifestUrl: string, + name?: string, + endpointName?: string +) => { + try { + const { snapshot, set } = callbackHelpers; + const response = await httpClient.get(`/projects/generate-projectId`); + const projectId = response.data; + const stringified = stringify({ + url: manifestUrl, + }); + const manifestResponse = await httpClient.get( + `/projects/${projectId}/skill/retrieve-skill-manifest?${stringified}` + ); + set(projectMetaDataState(projectId), { + isRootBot: false, + isRemote: true, + }); + set(botNameState(projectId), manifestResponse.data.name); + set(locationState(projectId), manifestUrl); + const dispatcher = await snapshot.getPromise(dispatcherState); + dispatcher.updateSkillManifest({ id: manifestResponse.data.id, content: manifestResponse.data }, projectId); + return projectId; + } catch (ex) { + console.log(ex); + //TODO: Handle Exception + } +}; + +export const openLocalSkill = async (callbackHelpers, pathToBot: string, storageId) => { + const { set } = callbackHelpers; + const { projectData, botFiles, error } = await fetchProjectDataByPath(pathToBot, storageId); + if (!error) { + await initBotState(callbackHelpers, projectData, botFiles); + set(projectMetaDataState(projectData.id), { + isRootBot: false, + isRemote: false, + }); + return projectData.id; + } else { + // handle error + } +}; + +export const createNewBotFromTemplate = async ( + callbackHelpers, + templateId: string, + name: string, + description: string, + location: string, + schemaUrl?: string, + locale?: string +) => { + const response = await httpClient.post(`/projects`, { + storageId: 'default', + templateId, + name, + description, + location, + schemaUrl, + locale, + }); + const { set } = callbackHelpers; + const projectId = response.data.id; + if (settingStorage.get(projectId)) { + settingStorage.remove(projectId); + } + const mainDialog = await initBotState(callbackHelpers, response.data, true); + set(botProjectSpaceLoadedState, true); + return { projectId, mainDialog }; +}; + +const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => { + set(botProjectSpaceProjectIds, (current: string[]) => { + const botProjectIDs = [...current, projectId]; + if (botProjectIDs.length === skillCt) { + set(botProjectSpaceLoadedState, true); + } + return botProjectIDs; + }); +}; + +export const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, projectData, botFiles, storageId) => { + const { set } = callbackHelpers; + set(botOpeningState, true); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); + const rootBotProjectId = projectData.id; + if (botFiles.botProjectSpaceFiles.length) { + const currentBotProjectFile: BotProjectSpace = botFiles.botProjectSpaceFiles[0]; + set(botProjectFileState(rootBotProjectId), currentBotProjectFile); + + const skillsInBotProject = currentBotProjectFile.skills; + if (skillsInBotProject.length) { + for (const skillInBotProject of skillsInBotProject) { + if (!skillInBotProject.remote) { + const path = parseFileProtocolPaths(currentBotProjectFile.workspace, skillInBotProject.workspace); + //TODO handle exception + openLocalSkill(callbackHelpers, path, storageId).then((projectId: string) => + addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length) + ); + } else { + //TODO handle exception + openRemoteSkill(callbackHelpers, skillInBotProject.manifest).then((projectId: string) => + addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length) + ); + } + } + } + } else { + set(botProjectSpaceLoadedState, true); + } + //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject + set(botProjectSpaceProjectIds, [rootBotProjectId]); + set(botOpeningState, false); + set(currentProjectIdState, rootBotProjectId); + navigateToBot(rootBotProjectId, mainDialog); }; diff --git a/Composer/packages/client/src/recoilModel/selectors/design.ts b/Composer/packages/client/src/recoilModel/selectors/design.ts index 8ffca593eb..424beed664 100644 --- a/Composer/packages/client/src/recoilModel/selectors/design.ts +++ b/Composer/packages/client/src/recoilModel/selectors/design.ts @@ -3,13 +3,13 @@ import { selector } from 'recoil'; -import { botNameState, botProjectsSpaceState, dialogsState } from '../atoms'; +import { botNameState, botProjectSpaceProjectIds, dialogsState } from '../atoms'; //TODO: This selector will be used when BotProjects is implemented export const botProjectSpaceSelector = selector({ key: 'botProjectSpaceSelector', get: ({ get }) => { - const botProjects = get(botProjectsSpaceState); + const botProjects = get(botProjectSpaceProjectIds); const result = botProjects.map((botProjectId: string) => { const dialogs = get(dialogsState(botProjectId)); const name = get(botNameState(botProjectId)); diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts new file mode 100644 index 0000000000..cbf49639cc --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { selector } from 'recoil'; + +import { dispatcherState } from '../DispatcherWrapper'; +import { Dispatcher } from '../dispatchers'; +import { botProjectSpaceProjectIds, projectMetaDataState } from '../atoms'; + +// Actions +const projectLoadAction = (dispatcher: Dispatcher) => { + return { + createProject: async ( + templateId: string, + name: string, + description: string, + location: string, + schemaUrl?: string, + locale?: string, + qnaKbUrls?: string[] + ) => { + await dispatcher.createProject(templateId, name, description, location, schemaUrl, locale, qnaKbUrls); + }, + openProject: async (path: string, storageId = 'default') => { + await dispatcher.openProject(path, storageId); + }, + saveProjectAs: async (projectId: string, name: string, description: string, location: string) => { + await dispatcher.saveProjectAs(projectId, name, description, location); + }, + fetchProjectById: async (projectId: string) => { + await dispatcher.fetchProjectById(projectId); + }, + addExistingSkillToBotProject: async (path: string, storageId = 'default') => { + const projectId: string = await dispatcher.addExistingSkillToBotProject(path, storageId); + + await dispatcher.addSkillToBotProject(projectId); + }, + addNewSkillToBotProject: async ( + templateId: string, + name: string, + description: string, + location: string, + schemaUrl?: string, + locale?: string, + qnaKbUrls?: string[] + ) => { + await dispatcher.addNewSkillToBotProject(templateId, name, description, location, schemaUrl, locale, qnaKbUrls); + }, + addRemoteSkillToBotProject: async (manifestUrl: string, name: string, endpointName: string) => { + await dispatcher.addRemoteSkillToBotProject(manifestUrl, name, endpointName); + }, + }; +}; + +export const projectLoadSelector = selector({ + key: 'projectLoadSelector', + get: ({ get }) => { + const dispatcher = get(dispatcherState); + if (!dispatcher) { + return {} as Dispatcher; + } + return projectLoadAction(dispatcher); + }, +}); + +export const projectMetaDataSelector = selector({ + key: 'projectMetaDataSelector', + get: ({ get }) => { + const botProjectIds = get(botProjectSpaceProjectIds); + return botProjectIds.map((projectId: string) => { + const metaData = get(projectMetaDataState(projectId)); + return { + projectId, + ...metaData, + }; + }); + }, +}); diff --git a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx index e64a06987e..6f046da8ad 100644 --- a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx +++ b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx @@ -13,14 +13,14 @@ import { luFilesState, projectMetaDataState, currentProjectIdState, - botProjectsSpaceState, + botProjectSpaceProjectIds, } from '../../atoms'; import { renderRecoilHook } from '../../../../__tests__/testUtils/react-recoil-hooks-testing-library'; import UndoHistory from '../undoHistory'; const projectId = '123-asd'; export const UndoRedoWrapper = () => { - const botProjects = useRecoilValue(botProjectsSpaceState); + const botProjects = useRecoilValue(botProjectSpaceProjectIds); return botProjects.length > 0 ? : null; }; @@ -59,7 +59,7 @@ describe('', () => { ); }, states: [ - { recoilState: botProjectsSpaceState, initialValue: [projectId] }, + { recoilState: botProjectSpaceProjectIds, initialValue: [projectId] }, { recoilState: dialogsState(projectId), initialValue: [{ id: '1' }] }, { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] }, { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] }, diff --git a/Composer/packages/client/src/recoilModel/undo/history.ts b/Composer/packages/client/src/recoilModel/undo/history.ts index b36e4f16e4..1a431b8212 100644 --- a/Composer/packages/client/src/recoilModel/undo/history.ts +++ b/Composer/packages/client/src/recoilModel/undo/history.ts @@ -9,6 +9,7 @@ import { } from 'recoil'; import { atomFamily, Snapshot, useRecoilCallback, CallbackInterface, useSetRecoilState } from 'recoil'; import uniqueId from 'lodash/uniqueId'; +import isEmpty from 'lodash/isEmpty'; import { navigateTo, getUrlSearch } from '../../utils/navigation'; @@ -140,10 +141,12 @@ export const UndoRoot = React.memo((props: UndoRootProps) => { }); const setInitialProjectState = useRecoilCallback(({ snapshot }: CallbackInterface) => () => { - undoHistory.clear(); - const assetMap = getAtomAssetsMap(snapshot, projectId); - undoHistory.add(assetMap); - setInitialStateLoaded(true); + if (!isEmpty(undoHistory)) { + undoHistory.clear(); + const assetMap = getAtomAssetsMap(snapshot, projectId); + undoHistory.add(assetMap); + setInitialStateLoaded(true); + } }); useEffect(() => { diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index f24f4013a8..547af8a8d9 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -12,7 +12,13 @@ import { resolveToBasePath } from './utils/fileUtil'; import { data } from './styles'; import { NotFound } from './components/NotFound'; import { BASEPATH } from './constants'; -import { dispatcherState, schemasState, botProjectsSpaceState, botOpeningState } from './recoilModel'; +import { + dispatcherState, + schemasState, + botProjectSpaceProjectIds, + botProjectSpaceLoadedState, + botOpeningState, +} from './recoilModel'; import { openAlertModal } from './components/Modal/AlertDialog'; import { dialogStyle } from './components/Modal/dialogStyle'; import { LoadingSpinner } from './components/LoadingSpinner'; @@ -89,8 +95,8 @@ const ProjectRouter: React.FC> = (pro const { projectId = '' } = props; const schemas = useRecoilValue(schemasState(projectId)); const { fetchProjectById } = useRecoilValue(dispatcherState); - const botProjects = useRecoilValue(botProjectsSpaceState); - const botOpening = useRecoilValue(botOpeningState); + const botProjects = useRecoilValue(botProjectSpaceProjectIds); + const botsLoaded = useRecoilValue(botProjectSpaceLoadedState); useEffect(() => { if (props.projectId && !botProjects.includes(props.projectId)) { @@ -107,7 +113,7 @@ const ProjectRouter: React.FC> = (pro } }, [schemas, projectId]); - if (props.projectId && !botOpening && botProjects.includes(props.projectId)) { + if (props.projectId && botsLoaded && botProjects.includes(props.projectId)) { return
{props.children}
; } return ; diff --git a/Composer/packages/client/src/utils/fileUtil.ts b/Composer/packages/client/src/utils/fileUtil.ts index 24f8256f28..1f324737eb 100644 --- a/Composer/packages/client/src/utils/fileUtil.ts +++ b/Composer/packages/client/src/utils/fileUtil.ts @@ -103,3 +103,11 @@ export async function loadLocale(locale: string) { }); } } + +export const trimFileProtocol = (path: string) => { + return path.replace('file://', ''); +}; + +export const convertPathToFileProtocol = (path: string) => { + return `file://${path}`; +}; diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 02a0b2cc9c..0ab40c4ab1 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -191,10 +191,10 @@ export interface BotInfo { } export interface BotProjectSpaceSkill { - workspace: string; - manifest: string; + workspace?: string; + manifest?: string; remote: boolean; - endpointName: string; + endpointName?: string; } export interface BotProjectSpace { diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 61956e4727..fac076e81a 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -207,6 +207,18 @@ async function getRecentProjects(req: Request, res: Response) { return res.status(200).json(projects); } +async function generateProjectId(req: Request, res: Response) { + try { + const location = req.query.location; + const projectId = await BotProjectService.generateProjectId(location); + res.status(200).json(projectId); + } catch (ex) { + res.status(404).json({ + message: 'Cannot generate project id', + }); + } +} + async function updateFile(req: Request, res: Response) { const projectId = req.params.projectId; const user = await PluginLoader.getUserFromRequest(req); @@ -253,22 +265,12 @@ async function removeFile(req: Request, res: Response) { } async function getSkill(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 content = await getSkillManifest(req.query.url); - res.status(200).json(content); - } catch (err) { - res.status(404).json({ - message: err.message, - }); - } - } else { + try { + const content = await getSkillManifest(req.query.url); + res.status(200).json(content); + } catch (err) { res.status(404).json({ - message: 'No such bot project opened', + message: err.message, }); } } @@ -411,4 +413,5 @@ export const ProjectController = { getRecentProjects, updateBoilerplate, checkBoilerplateVersion, + generateProjectId, }; diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index e28ee00cef..b6632b784d 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -20,6 +20,7 @@ const router: Router = express.Router({}); router.post('/projects', ProjectController.createProject); router.get('/projects', ProjectController.getAllProjects); router.get('/projects/recent', ProjectController.getRecentProjects); +router.get('/projects/generate-projectId', ProjectController.generateProjectId); router.get('/projects/:projectId', ProjectController.getProjectById); router.put('/projects/open', ProjectController.openProject); From 29044a0cfd1a957e084abcbf6f88d639518f4004 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Thu, 24 Sep 2020 10:54:51 -0700 Subject: [PATCH 006/100] More updates Signed-off-by: Srinaath Ravichandran --- Composer/package.json | 2 +- .../src/recoilModel/DispatcherWrapper.tsx | 4 +- .../client/src/recoilModel/atoms/botState.ts | 7 +++- .../dispatchers/__tests__/project.test.tsx | 8 ++-- .../recoilModel/dispatchers/botProjectFile.ts | 2 + .../src/recoilModel/dispatchers/project.ts | 17 ++++---- .../recoilModel/dispatchers/utils/project.ts | 39 ++++++++++--------- .../client/src/recoilModel/selectors/index.ts | 1 + .../src/recoilModel/selectors/project.ts | 22 +++++++++-- .../packages/lib/shared/src/types/indexers.ts | 1 + 10 files changed, 63 insertions(+), 40 deletions(-) diff --git a/Composer/package.json b/Composer/package.json index 4f5d112940..01e987223e 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "build:plugins:runtimes": "cd plugins/runtimes && yarn install && yarn build", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", "startall": "yarn start", - "start:dev": "concurrently \"npm:start:client\" \"npm:start:server\"", + "start:dev": "concurrently \"npm:start:client\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", "start:server:dev": "yarn workspace @bfc/server start:dev", diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index e5aaf1c48b..62da395850 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -25,7 +25,7 @@ import { projectMetaDataState, botProjectSpaceLoadedState, } from './atoms'; -import { projectMetaDataSelector } from './selectors/project'; +import { botProjectsWithoutErrorsSelector } from './selectors'; const getBotAssets = async (projectId, snapshot: Snapshot): Promise => { const result = await Promise.all([ @@ -85,7 +85,7 @@ const InitDispatcher = ({ onLoad }) => { export const DispatcherWrapper = ({ children }) => { const [loaded, setLoaded] = useState(false); - const botProjects = useRecoilValue(projectMetaDataSelector); + const botProjects = useRecoilValue(botProjectsWithoutErrorsSelector); const botProjectLoaded = useRecoilValue(botProjectSpaceLoadedState); useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => { diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index e9b75d2ad2..047e5c01dd 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -265,7 +265,12 @@ export const botProjectFileState = atomFamily({ }); // Always using the first manifest file for now. We dont have the need to support multiple manifest for a skill -export const currentManifestIndex = atomFamily({ +export const currentManifestIndexState = atomFamily({ key: getFullyQualifiedKey('currentManifestIndexState'), default: 0, }); + +export const botErrorState = atomFamily({ + key: getFullyQualifiedKey('botError'), + default: undefined, +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index e48af4afe7..7387817b33 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -87,7 +87,6 @@ describe('Project dispatcher', () => { const locale = useRecoilValue(localeState(projectId)); const botStatus = useRecoilValue(botStatusState(projectId)); - const botOpening = useRecoilValue(botOpeningState); const currentDispatcher = useRecoilValue(dispatcherState); const [recentProjects, setRecentProjects] = useRecoilState(recentProjectsState); const appError = useRecoilValue(applicationErrorState); @@ -98,7 +97,6 @@ describe('Project dispatcher', () => { const runtimeTemplates = useRecoilValue(runtimeTemplatesState); return { - botOpening, skillManifests, luFiles, lgFiles, @@ -161,7 +159,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.luFiles.length).toBe(1); expect(renderedComponent.current.botEnvironment).toBe(mockProjectResponse.botEnvironment); expect(renderedComponent.current.skills.length).toBe(0); - expect(renderedComponent.current.botOpening).toBeFalsy(); + // expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.schemas.sdk).toBeDefined(); expect(renderedComponent.current.schemas.default).toBeDefined(); expect(renderedComponent.current.schemas.diagnostics?.length).toBe(0); @@ -183,7 +181,7 @@ describe('Project dispatcher', () => { ]); await dispatcher.openProject('../test/empty-bot', 'default'); }); - expect(renderedComponent.current.botOpening).toBeFalsy(); + // expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.appError).toEqual(errorObj); expect(renderedComponent.current.recentProjects.length).toBe(0); expect(navigateTo).not.toHaveBeenCalled(); @@ -245,7 +243,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.luFiles.length).toBe(0); expect(renderedComponent.current.botEnvironment).toBe('production'); expect(renderedComponent.current.skills.length).toBe(0); - expect(renderedComponent.current.botOpening).toBeFalsy(); + // expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.schemas.sdk).toBeUndefined(); expect(renderedComponent.current.schemas.default).toBeUndefined(); expect(renderedComponent.current.schemas.diagnostics?.length).toBeUndefined(); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index 88343ed57f..bc9f8a96d5 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -13,6 +13,7 @@ export const botProjectFileDispatcher = () => { const addSkillToBotProject = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { const skillLocation = await snapshot.getPromise(locationState(projectId)); + const botName = await snapshot.getPromise(botNameState(projectId)); const manifests: { id: string; content: string; lastModified: string }[] = await snapshot.getPromise( skillManifestsState(projectId) ); @@ -25,6 +26,7 @@ export const botProjectFileDispatcher = () => { manifest: currentManifest.id, workspace: convertPathToFileProtocol(skillLocation), remote: false, + name: botName, }; draftState.skills.push(skill); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index b0c2022301..e77f3e6498 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -45,19 +45,14 @@ export const projectDispatcher = () => { const addExistingSkillToBotProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { - const { set } = callbackHelpers; const projectId = await openLocalSkill(callbackHelpers, path, storageId); - set(botProjectSpaceProjectIds, (current) => [...current, projectId]); + return projectId; } ); const addRemoteSkillToBotProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { - try { - openRemoteSkill(callbackHelpers, manifestUrl, name, endpointName); - } catch (ex) { - // Handle exception - } + openRemoteSkill(callbackHelpers, manifestUrl, name); } ); @@ -71,8 +66,8 @@ export const projectDispatcher = () => { locale?: string, qnaKbUrls?: string[] ) => { + const { set } = callbackHelpers; try { - const { set } = callbackHelpers; const { projectId, mainDialog } = await createNewBotFromTemplate( callbackHelpers, templateId, @@ -84,8 +79,10 @@ export const projectDispatcher = () => { ); set(botProjectSpaceProjectIds, (current) => [...current, projectId]); navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); + return projectId; } catch (ex) { - // Handle exception in opening a skill + set(botProjectSpaceProjectIds, []); + handleProjectFailure(callbackHelpers, ex); } } ); @@ -146,7 +143,7 @@ export const projectDispatcher = () => { return projectId; } catch (ex) { set(botProjectSpaceProjectIds, []); - removeRecentProject(callbackHelpers, path); + removeRecentProject(callbackHelpers, location); handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index c10034af05..9e41dd10ff 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -3,6 +3,7 @@ import path from 'path'; +import { v4 as uuid } from 'uuid'; import { SensitiveProperties, convertSkillsToDictionary, @@ -34,6 +35,7 @@ import { qnaFilesState, recentProjectsState, botOpeningState, + botErrorState, } from '../../atoms'; import lgWorker from '../../parsers/lgWorker'; import luWorker from '../../parsers/luWorker'; @@ -63,7 +65,6 @@ import { dialogSchemasState, } from '../../atoms'; import { undoHistoryState } from '../../undo/history'; -import { dispatcherState } from '../../DispatcherWrapper'; export const resetBotStates = async ({ snapshot, gotoSnapshot }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); @@ -325,14 +326,9 @@ export const removeRecentProject = async (callbackHelpers: CallbackInterface, pa } }; -export const openRemoteSkill = async ( - callbackHelpers: CallbackInterface, - manifestUrl: string, - name?: string, - endpointName?: string -) => { +export const openRemoteSkill = async (callbackHelpers: CallbackInterface, manifestUrl: string, name: string) => { + const { set } = callbackHelpers; try { - const { snapshot, set } = callbackHelpers; const response = await httpClient.get(`/projects/generate-projectId`); const projectId = response.data; const stringified = stringify({ @@ -347,16 +343,16 @@ export const openRemoteSkill = async ( }); set(botNameState(projectId), manifestResponse.data.name); set(locationState(projectId), manifestUrl); - const dispatcher = await snapshot.getPromise(dispatcherState); - dispatcher.updateSkillManifest({ id: manifestResponse.data.id, content: manifestResponse.data }, projectId); return projectId; } catch (ex) { - console.log(ex); - //TODO: Handle Exception + const tempProjectId = uuid(); + set(botNameState(tempProjectId), name); + set(botErrorState(tempProjectId), ex); + return tempProjectId; } }; -export const openLocalSkill = async (callbackHelpers, pathToBot: string, storageId) => { +export const openLocalSkill = async (callbackHelpers, pathToBot: string, storageId, name?: string) => { const { set } = callbackHelpers; const { projectData, botFiles, error } = await fetchProjectDataByPath(pathToBot, storageId); if (!error) { @@ -367,7 +363,10 @@ export const openLocalSkill = async (callbackHelpers, pathToBot: string, storage }); return projectData.id; } else { - // handle error + const tempProjectId = uuid(); + set(botNameState(tempProjectId), name); + set(botErrorState(tempProjectId), error); + return tempProjectId; } }; @@ -424,14 +423,18 @@ export const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, p if (!skillInBotProject.remote) { const path = parseFileProtocolPaths(currentBotProjectFile.workspace, skillInBotProject.workspace); //TODO handle exception - openLocalSkill(callbackHelpers, path, storageId).then((projectId: string) => + openLocalSkill(callbackHelpers, path, skillInBotProject.name, storageId).then((projectId: string) => addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length) ); } else { //TODO handle exception - openRemoteSkill(callbackHelpers, skillInBotProject.manifest).then((projectId: string) => - addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length) - ); + if (skillInBotProject.manifest) { + openRemoteSkill( + callbackHelpers, + skillInBotProject.manifest, + skillInBotProject.name + ).then((projectId: string) => addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length)); + } } } } diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index 2679b02214..0d1bb3804f 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -4,3 +4,4 @@ export * from '../selectors/eject'; export * from '../selectors/design'; export * from '../selectors/validatedDialogs'; +export * from '../selectors/project'; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index cbf49639cc..a6435d0ecb 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -5,7 +5,7 @@ import { selector } from 'recoil'; import { dispatcherState } from '../DispatcherWrapper'; import { Dispatcher } from '../dispatchers'; -import { botProjectSpaceProjectIds, projectMetaDataState } from '../atoms'; +import { botErrorState, botProjectSpaceProjectIds, projectMetaDataState } from '../atoms'; // Actions const projectLoadAction = (dispatcher: Dispatcher) => { @@ -63,8 +63,24 @@ export const projectLoadSelector = selector({ }, }); -export const projectMetaDataSelector = selector({ - key: 'projectMetaDataSelector', +export const botProjectsWithoutErrorsSelector = selector({ + key: 'botProjectsWithoutErrorsSelector', + get: ({ get }) => { + const botProjectIds = get(botProjectSpaceProjectIds); + return botProjectIds + .filter((projectId) => !get(botErrorState(projectId))) + .map((projectId: string) => { + const metaData = get(projectMetaDataState(projectId)); + return { + projectId, + ...metaData, + }; + }); + }, +}); + +export const botProjectsDataSelector = selector({ + key: 'botProjectsDataSelector', get: ({ get }) => { const botProjectIds = get(botProjectSpaceProjectIds); return botProjectIds.map((projectId: string) => { diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 0ab40c4ab1..c4dfe83591 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -195,6 +195,7 @@ export interface BotProjectSpaceSkill { manifest?: string; remote: boolean; endpointName?: string; + name: string; } export interface BotProjectSpace { From 28f917c2a29f99e528cc324929a7e73c1aabaf5e Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Thu, 24 Sep 2020 16:08:22 -0700 Subject: [PATCH 007/100] More updates Signed-off-by: Srinaath Ravichandran --- .../recoilModel/dispatchers/botProjectFile.ts | 28 ++++++------ .../src/recoilModel/dispatchers/project.ts | 3 +- .../recoilModel/dispatchers/utils/project.ts | 14 +++++- .../src/recoilModel/selectors/design.ts | 21 --------- .../client/src/recoilModel/selectors/index.ts | 1 - .../src/recoilModel/selectors/project.ts | 43 ++++++++++++------- 6 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 Composer/packages/client/src/recoilModel/selectors/design.ts diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index bc9f8a96d5..623d6aa703 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -11,23 +11,25 @@ import { convertPathToFileProtocol, trimFileProtocol } from '../../utils/fileUti export const botProjectFileDispatcher = () => { const addSkillToBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { - const skillLocation = await snapshot.getPromise(locationState(projectId)); - const botName = await snapshot.getPromise(botNameState(projectId)); + ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, skillId: string, remote: boolean) => { + const skillLocation = await snapshot.getPromise(locationState(skillId)); + const botName = await snapshot.getPromise(botNameState(skillId)); const manifests: { id: string; content: string; lastModified: string }[] = await snapshot.getPromise( - skillManifestsState(projectId) + skillManifestsState(skillId) ); // TODO:// We would support only 1 manifest per skill. It will always be the first manifest. We would need UI in future to set the default manifest file const currentManifest = manifests[0]; - set(botProjectFileState(projectId), (current: BotProjectSpace) => { + set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { const result = produce(current, (draftState: BotProjectSpace) => { const skill: BotProjectSpaceSkill = { - manifest: currentManifest.id, workspace: convertPathToFileProtocol(skillLocation), - remote: false, + remote, name: botName, }; + if (currentManifest) { + skill.manifest = currentManifest.id; + } draftState.skills.push(skill); }); return result; @@ -36,11 +38,11 @@ export const botProjectFileDispatcher = () => { ); const removeLocalSkillFromBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { - const skillLocation = await snapshot.getPromise(locationState(projectId)); - snapshot.getPromise(skillManifestsState(projectId)); + ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, skillId: string) => { + const skillLocation = await snapshot.getPromise(locationState(skillId)); + snapshot.getPromise(skillManifestsState(skillId)); - set(botProjectFileState(projectId), (current: BotProjectSpace) => { + set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { const result = produce(current, (draftState: BotProjectSpace) => { draftState.skills = draftState.skills.filter(({ workspace }) => { if (workspace) { @@ -55,9 +57,9 @@ export const botProjectFileDispatcher = () => { ); const removeRemoteSkillFromBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, projectId: string) => { const manifestLocation = await snapshot.getPromise(locationState(projectId)); - set(botProjectFileState(projectId), (current: BotProjectSpace) => { + set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { const result = produce(current, (draftState: BotProjectSpace) => { draftState.skills = draftState.skills.filter(({ manifest, remote }) => { if (remote) { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index e77f3e6498..1dcead710a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -52,7 +52,8 @@ export const projectDispatcher = () => { const addRemoteSkillToBotProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { - openRemoteSkill(callbackHelpers, manifestUrl, name); + const projectId = await openRemoteSkill(callbackHelpers, manifestUrl, name); + return projectId; } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 9e41dd10ff..0e4a369334 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -77,6 +77,16 @@ export const resetBotStates = async ({ snapshot, gotoSnapshot }: CallbackInterfa gotoSnapshot(newSnapshot); }; +export const setErrorBotProject = async (callbackHelpers: CallbackInterface, projectId, botName, payload) => { + const { set } = callbackHelpers; + if (payload?.response?.data?.message) { + set(botErrorState(projectId), payload.response.data); + } else { + set(botErrorState(projectId), payload); + } + if (payload != null) logMessage(callbackHelpers, `Error loading ${botName}: ${JSON.stringify(payload)}`); +}; + export const flushExistingTasks = async (callbackHelpers) => { const { snapshot, reset } = callbackHelpers; const projectIds = await snapshot.getPromise(botProjectSpaceProjectIds); @@ -347,7 +357,7 @@ export const openRemoteSkill = async (callbackHelpers: CallbackInterface, manife } catch (ex) { const tempProjectId = uuid(); set(botNameState(tempProjectId), name); - set(botErrorState(tempProjectId), ex); + setErrorBotProject(callbackHelpers, tempProjectId, name, ex); return tempProjectId; } }; @@ -365,7 +375,7 @@ export const openLocalSkill = async (callbackHelpers, pathToBot: string, storage } else { const tempProjectId = uuid(); set(botNameState(tempProjectId), name); - set(botErrorState(tempProjectId), error); + setErrorBotProject(callbackHelpers, tempProjectId, name, error); return tempProjectId; } }; diff --git a/Composer/packages/client/src/recoilModel/selectors/design.ts b/Composer/packages/client/src/recoilModel/selectors/design.ts deleted file mode 100644 index 424beed664..0000000000 --- a/Composer/packages/client/src/recoilModel/selectors/design.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { selector } from 'recoil'; - -import { botNameState, botProjectSpaceProjectIds, dialogsState } from '../atoms'; - -//TODO: This selector will be used when BotProjects is implemented -export const botProjectSpaceSelector = selector({ - key: 'botProjectSpaceSelector', - get: ({ get }) => { - const botProjects = get(botProjectSpaceProjectIds); - const result = botProjects.map((botProjectId: string) => { - const dialogs = get(dialogsState(botProjectId)); - const name = get(botNameState(botProjectId)); - const projectId = botProjectId; - return { dialogs, projectId, name }; - }); - return result; - }, -}); diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index 0d1bb3804f..45512efcb5 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -2,6 +2,5 @@ // Licensed under the MIT License. export * from '../selectors/eject'; -export * from '../selectors/design'; export * from '../selectors/validatedDialogs'; export * from '../selectors/project'; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index a6435d0ecb..2cf37a16d3 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -5,7 +5,7 @@ import { selector } from 'recoil'; import { dispatcherState } from '../DispatcherWrapper'; import { Dispatcher } from '../dispatchers'; -import { botErrorState, botProjectSpaceProjectIds, projectMetaDataState } from '../atoms'; +import { botErrorState, botNameState, botProjectSpaceProjectIds, dialogsState, projectMetaDataState } from '../atoms'; // Actions const projectLoadAction = (dispatcher: Dispatcher) => { @@ -30,12 +30,12 @@ const projectLoadAction = (dispatcher: Dispatcher) => { fetchProjectById: async (projectId: string) => { await dispatcher.fetchProjectById(projectId); }, - addExistingSkillToBotProject: async (path: string, storageId = 'default') => { - const projectId: string = await dispatcher.addExistingSkillToBotProject(path, storageId); - - await dispatcher.addSkillToBotProject(projectId); + addExistingSkillToBotProject: async (rootBotId: string, path: string, storageId = 'default') => { + const skillId: string = await dispatcher.addExistingSkillToBotProject(path, storageId); + await dispatcher.addSkillToBotProject(rootBotId, skillId, false); }, addNewSkillToBotProject: async ( + rootBotId: string, templateId: string, name: string, description: string, @@ -44,10 +44,20 @@ const projectLoadAction = (dispatcher: Dispatcher) => { locale?: string, qnaKbUrls?: string[] ) => { - await dispatcher.addNewSkillToBotProject(templateId, name, description, location, schemaUrl, locale, qnaKbUrls); + const skillId: string = await dispatcher.addNewSkillToBotProject( + templateId, + name, + description, + location, + schemaUrl, + locale, + qnaKbUrls + ); + await dispatcher.addSkillToBotProject(rootBotId, skillId, false); }, - addRemoteSkillToBotProject: async (manifestUrl: string, name: string, endpointName: string) => { - await dispatcher.addRemoteSkillToBotProject(manifestUrl, name, endpointName); + addRemoteSkillToBotProject: async (rootBotId: string, manifestUrl: string, name: string, endpointName: string) => { + const skillId = await dispatcher.addRemoteSkillToBotProject(manifestUrl, name, endpointName); + await dispatcher.addSkillToBotProject(rootBotId, skillId, true); }, }; }; @@ -79,16 +89,17 @@ export const botProjectsWithoutErrorsSelector = selector({ }, }); -export const botProjectsDataSelector = selector({ - key: 'botProjectsDataSelector', +export const botProjectSpaceSelector = selector({ + key: 'botProjectSpaceSelector', get: ({ get }) => { - const botProjectIds = get(botProjectSpaceProjectIds); - return botProjectIds.map((projectId: string) => { + const botProjects = get(botProjectSpaceProjectIds); + const result = botProjects.map((projectId: string) => { + const dialogs = get(dialogsState(projectId)); const metaData = get(projectMetaDataState(projectId)); - return { - projectId, - ...metaData, - }; + const botError = get(botErrorState(projectId)); + const name = get(botNameState(projectId)); + return { dialogs, projectId, name, ...metaData, error: botError }; }); + return result; }, }); From 0b6aa25aae84136e539c343f86cef2df3c675a3d Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Fri, 25 Sep 2020 21:26:24 +0800 Subject: [PATCH 008/100] update the recoil to 0.0.13 --- Composer/packages/adaptive-flow/package.json | 6 ++-- Composer/packages/adaptive-form/package.json | 8 +++--- Composer/packages/client/package.json | 6 ++-- .../src/recoilModel/DispatcherWrapper.tsx | 6 ++-- .../dispatchers/__tests__/testUtils.ts | 1 + .../packages/extension-client/package.json | 8 +++--- Composer/packages/intellisense/package.json | 4 +-- .../packages/lib/code-editor/package.json | 8 +++--- Composer/packages/lib/shared/package.json | 8 +++--- Composer/packages/lib/ui-shared/package.json | 8 +++--- .../packages/ui-plugins/composer/package.json | 8 +++--- .../ui-plugins/cross-trained/package.json | 8 +++--- .../packages/ui-plugins/example/package.json | 8 +++--- Composer/packages/ui-plugins/lg/package.json | 8 +++--- .../packages/ui-plugins/luis/package.json | 8 +++--- .../packages/ui-plugins/prompts/package.json | 8 +++--- .../ui-plugins/schema-editor/package.json | 8 +++--- .../ui-plugins/select-dialog/package.json | 8 +++--- .../select-skill-dialog/package.json | 8 +++--- Composer/yarn.lock | 28 +++++++++---------- 20 files changed, 83 insertions(+), 80 deletions(-) diff --git a/Composer/packages/adaptive-flow/package.json b/Composer/packages/adaptive-flow/package.json index 7c2cdc9ddc..3a3dcb43e2 100644 --- a/Composer/packages/adaptive-flow/package.json +++ b/Composer/packages/adaptive-flow/package.json @@ -40,7 +40,7 @@ }, "peerDependencies": { "format-message": "^6.2.3", - "react": "16.13.0" + "react": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", @@ -48,8 +48,8 @@ "@types/react": "16.9.23", "format-message": "^6.2.3", "json-loader": "^0.5.7", - "react": "16.13.0", - "react-dom": "16.13.0", + "react": "16.13.1", + "react-dom": "16.13.1", "ts-loader": "7.0.4", "tsconfig-paths-webpack-plugin": "^3.2.0" }, diff --git a/Composer/packages/adaptive-form/package.json b/Composer/packages/adaptive-form/package.json index 50c4f1ab2d..2a91119915 100644 --- a/Composer/packages/adaptive-form/package.json +++ b/Composer/packages/adaptive-form/package.json @@ -26,8 +26,8 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/code-editor": "*", @@ -37,8 +37,8 @@ "@types/lodash": "^4.14.149", "@types/react": "16.9.23", "format-message": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@emotion/core": "^10.0.27", diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 8bb8e2289a..c55547f2d3 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -50,13 +50,13 @@ "prop-types": "^15.7.2", "query-string": "^6.8.2", "re-resizable": "^6.3.2", - "react": "16.13.0", + "react": "16.13.1", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.3", - "react-dom": "16.13.0", + "react-dom": "16.13.1", "react-frame-component": "^4.0.2", "react-timeago": "^4.4.0", - "recoil": "^0.0.10", + "recoil": "^0.0.13", "styled-components": "^4.1.3", "uuid": "^8.3.0", "webpack-bundle-analyzer": "^3.8.0" diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 055ec170e4..41400b4a24 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useRef, useEffect, useState, Fragment } from 'react'; +import { useRef, useEffect, useState, Fragment, useLayoutEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/camelcase import { atom, useRecoilTransactionObserver_UNSTABLE, Snapshot, useRecoilState } from 'recoil'; import once from 'lodash/once'; @@ -72,7 +72,9 @@ const InitDispatcher = ({ onLoad }) => { const [currentDispatcherState, setDispatcher] = useRecoilState(dispatcherState); - useEffect(() => { + //The render order is different with 0.0.10, the local state will trigger a render before atom value + //so use the useLayoutEffect here + useLayoutEffect(() => { setDispatcher(dispatcherRef.current); prepareAxiosWithRecoil(currentDispatcherState); onLoad(true); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/testUtils.ts b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/testUtils.ts index 7f09e63daa..5f9dfd17ca 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/testUtils.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/testUtils.ts @@ -10,6 +10,7 @@ export const mockCallback: CallbackInterface = { reset: jest.fn(), gotoSnapshot: jest.fn(), snapshot: { + getID: jest.fn(), getLoadable: jest.fn(), getPromise: jest.fn(), map: jest.fn(), diff --git a/Composer/packages/extension-client/package.json b/Composer/packages/extension-client/package.json index f5e3f627db..0948d57580 100644 --- a/Composer/packages/extension-client/package.json +++ b/Composer/packages/extension-client/package.json @@ -11,14 +11,14 @@ "clean": "rimraf lib" }, "peerDependencies": { - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", "@types/react": "16.9.23", - "react": "16.13.0", - "react-dom": "16.13.0", + "react": "16.13.1", + "react-dom": "16.13.1", "rimraf": "^3.0.2", "typescript": "3.9.2" }, diff --git a/Composer/packages/intellisense/package.json b/Composer/packages/intellisense/package.json index 54b41edafb..0dcb7ef5b1 100644 --- a/Composer/packages/intellisense/package.json +++ b/Composer/packages/intellisense/package.json @@ -10,8 +10,8 @@ }, "peerDependencies": { "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "websocket": "^1.0.31", diff --git a/Composer/packages/lib/code-editor/package.json b/Composer/packages/lib/code-editor/package.json index 1b96048a19..9a149f06b9 100644 --- a/Composer/packages/lib/code-editor/package.json +++ b/Composer/packages/lib/code-editor/package.json @@ -24,8 +24,8 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", @@ -42,8 +42,8 @@ "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.6.0", "postcss-loader": "^3.0.0", - "react-dom": "16.13.0", - "react": "16.13.0", + "react-dom": "16.13.1", + "react": "16.13.1", "rimraf": "^2.6.3", "style-loader": "^0.23.1", "ts-loader": "7.0.4", diff --git a/Composer/packages/lib/shared/package.json b/Composer/packages/lib/shared/package.json index e660e90577..c9a10931f0 100644 --- a/Composer/packages/lib/shared/package.json +++ b/Composer/packages/lib/shared/package.json @@ -21,8 +21,8 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", @@ -32,8 +32,8 @@ "autoprefixer": "^9.5.1", "copyfiles": "^2.1.0", "office-ui-fabric-react": "^7.88.1", - "react": "16.13.0", - "react-dom": "16.13.0", + "react": "16.13.1", + "react-dom": "16.13.1", "rimraf": "^2.6.3" }, "dependencies": { diff --git a/Composer/packages/lib/ui-shared/package.json b/Composer/packages/lib/ui-shared/package.json index 5ef946a1a3..5ad003d57b 100644 --- a/Composer/packages/lib/ui-shared/package.json +++ b/Composer/packages/lib/ui-shared/package.json @@ -24,8 +24,8 @@ "@emotion/core": "^10.0.27", "@emotion/styled": "^10.0.27", "@bfc/extension-client": "*", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -33,8 +33,8 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@bfc/shared": "*", diff --git a/Composer/packages/ui-plugins/composer/package.json b/Composer/packages/ui-plugins/composer/package.json index c00b527688..c59aa6fc15 100644 --- a/Composer/packages/ui-plugins/composer/package.json +++ b/Composer/packages/ui-plugins/composer/package.json @@ -24,8 +24,8 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -36,8 +36,8 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message-cli": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "lodash": "^4.17.19" diff --git a/Composer/packages/ui-plugins/cross-trained/package.json b/Composer/packages/ui-plugins/cross-trained/package.json index 382601c127..d60cf9b9b6 100644 --- a/Composer/packages/ui-plugins/cross-trained/package.json +++ b/Composer/packages/ui-plugins/cross-trained/package.json @@ -22,16 +22,16 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@bfc/shared": "*", diff --git a/Composer/packages/ui-plugins/example/package.json b/Composer/packages/ui-plugins/example/package.json index 2cf4bf54e3..3322fbc7c6 100644 --- a/Composer/packages/ui-plugins/example/package.json +++ b/Composer/packages/ui-plugins/example/package.json @@ -23,8 +23,8 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -33,8 +33,8 @@ "@emotion/core": "^10.0.27", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { } diff --git a/Composer/packages/ui-plugins/lg/package.json b/Composer/packages/ui-plugins/lg/package.json index e5586a41ef..142e105925 100644 --- a/Composer/packages/ui-plugins/lg/package.json +++ b/Composer/packages/ui-plugins/lg/package.json @@ -23,8 +23,8 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -32,8 +32,8 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@bfc/shared": "*", diff --git a/Composer/packages/ui-plugins/luis/package.json b/Composer/packages/ui-plugins/luis/package.json index e979418ea7..6eeb2588bf 100644 --- a/Composer/packages/ui-plugins/luis/package.json +++ b/Composer/packages/ui-plugins/luis/package.json @@ -24,8 +24,8 @@ "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", "@bfc/shared": "*", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -33,8 +33,8 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@bfc/shared": "*", diff --git a/Composer/packages/ui-plugins/prompts/package.json b/Composer/packages/ui-plugins/prompts/package.json index 803e95ff08..8d4d6e0840 100644 --- a/Composer/packages/ui-plugins/prompts/package.json +++ b/Composer/packages/ui-plugins/prompts/package.json @@ -28,8 +28,8 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/extension-client": "*", @@ -39,8 +39,8 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@bfc/shared": "*", diff --git a/Composer/packages/ui-plugins/schema-editor/package.json b/Composer/packages/ui-plugins/schema-editor/package.json index ce440bd681..caee894f71 100644 --- a/Composer/packages/ui-plugins/schema-editor/package.json +++ b/Composer/packages/ui-plugins/schema-editor/package.json @@ -24,8 +24,8 @@ "@bfc/adaptive-form": "*", "@bfc/code-editor": "*", "format-message": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/extension-client": "*", @@ -34,8 +34,8 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@emotion/core": "^10.0.27" diff --git a/Composer/packages/ui-plugins/select-dialog/package.json b/Composer/packages/ui-plugins/select-dialog/package.json index fded97e09a..b507dac159 100644 --- a/Composer/packages/ui-plugins/select-dialog/package.json +++ b/Composer/packages/ui-plugins/select-dialog/package.json @@ -28,8 +28,8 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/extension-client": "*", @@ -39,8 +39,8 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@emotion/core": "^10.0.27" diff --git a/Composer/packages/ui-plugins/select-skill-dialog/package.json b/Composer/packages/ui-plugins/select-skill-dialog/package.json index 3128f70498..c05e3d53fe 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/package.json +++ b/Composer/packages/ui-plugins/select-skill-dialog/package.json @@ -28,8 +28,8 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "devDependencies": { "@bfc/adaptive-form": "*", @@ -39,8 +39,8 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.0", - "react-dom": "16.13.0" + "react": "16.13.1", + "react-dom": "16.13.1" }, "dependencies": { "@emotion/core": "^10.0.27" diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 3e8beffec5..52c30a536e 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -15908,15 +15908,15 @@ react-dev-utils@^7.0.3: strip-ansi "5.0.0" text-table "0.2.0" -react-dom@16.13.0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.0.tgz#cdde54b48eb9e8a0ca1b3dc9943d9bb409b81866" - integrity sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg== +react-dom@16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" + integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.19.0" + scheduler "^0.19.1" react-error-boundary@^1.2.5: version "1.2.5" @@ -15978,10 +15978,10 @@ react-timeago@^4.4.0: resolved "https://botbuilder.myget.org/F/botbuilder-tools-daily/npm/react-timeago/-/react-timeago-4.4.0.tgz#4520dd9ba63551afc4d709819f52b14b9343ba2b" integrity sha1-RSDdm6Y1Ua/E1wmBn1KxS5NDuis= -react@16.13.0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7" - integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ== +react@16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -16161,10 +16161,10 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recoil@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.0.10.tgz#679ab22306f559f8a63c46fd5ff5241539f9248f" - integrity sha512-+9gRqehw3yKETmoZbhSnWu4GO10HDb5xYf1CjLF1oXGK2uT6GX5Lu9mfTXwjxV/jXxEKx8MIRUUbgPxvbJ8SEw== +recoil@^0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.0.13.tgz#23e6d63135c07d8defbd91aaa35dca94ec5c15a6" + integrity sha512-2OToaQ8GR//KsdKdaEhMi04QKStLGRpk3qjC58iBpZpUtsByZ4dUy2UJtRcYuhnVlltGZ8HNwcEQRdFOS864SQ== reconnecting-websocket@^3.2.2: version "3.2.2" @@ -16809,7 +16809,7 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.0, scheduler@^0.19.1: +scheduler@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== From 1230a205bda6f1c231b592c5ef4fc491a4db9ade Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Fri, 25 Sep 2020 21:34:17 +0800 Subject: [PATCH 009/100] fix lint --- Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 41400b4a24..892e7b7455 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useRef, useEffect, useState, Fragment, useLayoutEffect } from 'react'; +import { useRef, useState, Fragment, useLayoutEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/camelcase import { atom, useRecoilTransactionObserver_UNSTABLE, Snapshot, useRecoilState } from 'recoil'; import once from 'lodash/once'; From bee0818258500263bf76bb33e5e16aee62b12a97 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Fri, 25 Sep 2020 21:44:08 +0800 Subject: [PATCH 010/100] remove @type/recoil --- Composer/packages/client/package.json | 1 - Composer/yarn.lock | 7 ------- 2 files changed, 8 deletions(-) diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index c55547f2d3..357d8760f9 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -79,7 +79,6 @@ "@types/reach__router": "^1.2.4", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "@types/recoil": "^0.0.1", "@types/webpack-env": "^1.15.2", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.1.0", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 52c30a536e..4dcd187017 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -3811,13 +3811,6 @@ "@types/prop-types" "*" csstype "^2.2.0" -"@types/recoil@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@types/recoil/-/recoil-0.0.1.tgz#c69efee4871e0e3fc4fe19ba15a6bfc35f9b6bf1" - integrity sha512-pRrS1aizm7GqM9NciOSPGoQ7K56rhyMcha6/Zewkwnb/H1dmz7VbDs+RIE/QZOx9CBHqdFBAU4x6LYt+qJZV8A== - dependencies: - "@types/react" "*" - "@types/rimraf@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" From d491bac380e54a86a512250eed9c80a1d3403328 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 25 Sep 2020 10:13:04 -0700 Subject: [PATCH 011/100] router work Signed-off-by: Srinaath Ravichandran --- Composer/package.json | 2 +- .../__tests__/components/design.test.tsx | 2 +- .../__tests__/components/projecttree.test.tsx | 2 +- .../client/src/Onboarding/Onboarding.tsx | 9 +- .../components/ProjectTree/ProjectTree.tsx | 326 ++++++++++-------- .../src/components/ProjectTree/treeItem.tsx | 104 ++++-- .../client/src/recoilModel/atoms/appState.ts | 4 +- .../src/recoilModel/dispatchers/project.ts | 93 +++-- .../recoilModel/dispatchers/utils/project.ts | 96 +++--- .../src/recoilModel/selectors/project.ts | 13 +- .../undo/__test__/history.test.tsx | 6 +- Composer/packages/client/src/router.tsx | 4 +- 12 files changed, 389 insertions(+), 272 deletions(-) diff --git a/Composer/package.json b/Composer/package.json index 01e987223e..4f5d112940 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "build:plugins:runtimes": "cd plugins/runtimes && yarn install && yarn build", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", "startall": "yarn start", - "start:dev": "concurrently \"npm:start:client\"", + "start:dev": "concurrently \"npm:start:client\" \"npm:start:server\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", "start:server:dev": "yarn workspace @bfc/server start:dev", diff --git a/Composer/packages/client/__tests__/components/design.test.tsx b/Composer/packages/client/__tests__/components/design.test.tsx index 3231e33d7a..a93bab9484 100644 --- a/Composer/packages/client/__tests__/components/design.test.tsx +++ b/Composer/packages/client/__tests__/components/design.test.tsx @@ -7,7 +7,7 @@ import { DialogInfo } from '@bfc/shared'; import { renderWithRecoil } from '../testUtils'; import { dialogs } from '../constants.json'; -import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree'; +import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree-stable'; import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal'; import { CreateDialogModal } from '../../src/pages/design/createDialogModal'; diff --git a/Composer/packages/client/__tests__/components/projecttree.test.tsx b/Composer/packages/client/__tests__/components/projecttree.test.tsx index 217e1743b5..9e08b50523 100644 --- a/Composer/packages/client/__tests__/components/projecttree.test.tsx +++ b/Composer/packages/client/__tests__/components/projecttree.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { fireEvent } from '@bfc/test-utils'; import { dialogs } from '../constants.json'; -import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree'; +import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree-stable'; import { renderWithRecoil } from '../testUtils'; describe('', () => { diff --git a/Composer/packages/client/src/Onboarding/Onboarding.tsx b/Composer/packages/client/src/Onboarding/Onboarding.tsx index 13ebabe2be..50817bf654 100644 --- a/Composer/packages/client/src/Onboarding/Onboarding.tsx +++ b/Composer/packages/client/src/Onboarding/Onboarding.tsx @@ -9,12 +9,7 @@ import { useRecoilValue } from 'recoil'; import onboardingStorage from '../utils/onboardingStorage'; import { OpenConfirmModal } from '../components/Modal/ConfirmDialog'; import { useLocation } from '../utils/hooks'; -import { - dispatcherState, - onboardingState, - botProjectSpaceProjectIds, - validateDialogSelectorFamily, -} from '../recoilModel'; +import { dispatcherState, onboardingState, botProjectIdsState, validateDialogSelectorFamily } from '../recoilModel'; import OnboardingContext from './OnboardingContext'; import TeachingBubbles from './TeachingBubbles/TeachingBubbles'; @@ -25,7 +20,7 @@ const getCurrentSet = (stepSets) => stepSets.findIndex(({ id }) => id === onboar const Onboarding: React.FC = () => { const didMount = useRef(false); - const botProjects = useRecoilValue(botProjectSpaceProjectIds); + const botProjects = useRecoilValue(botProjectIdsState); const rootBotProjectId = botProjects[0]; const dialogs = useRecoilValue(validateDialogSelectorFamily(rootBotProjectId)); const { onboardingSetComplete } = useRecoilValue(dispatcherState); diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index 65399c06b3..d51dc20a0a 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -2,15 +2,8 @@ // Licensed under the MIT License. /** @jsx jsx */ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useState, Fragment } from 'react'; import { jsx, css } from '@emotion/core'; -import { - GroupedList, - IGroup, - IGroupHeaderProps, - IGroupRenderProps, - IGroupedList, -} from 'office-ui-fabric-react/lib/GroupedList'; import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox'; import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone'; import cloneDeep from 'lodash/cloneDeep'; @@ -19,10 +12,10 @@ import { DialogInfo, ITrigger } from '@bfc/shared'; import { Resizable, ResizeCallback } from 're-resizable'; import debounce from 'lodash/debounce'; import { useRecoilValue } from 'recoil'; -import { IGroupedListStyles } from 'office-ui-fabric-react/lib/GroupedList'; import { ISearchBoxStyles } from 'office-ui-fabric-react/lib/SearchBox'; import { dispatcherState, userSettingsState } from '../../recoilModel'; +import { botProjectSpaceSelector } from '../../recoilModel/selectors'; import { createSelectedPath, getFriendlyName } from '../../utils/dialogUtil'; import { containUnsupportedTriggers, triggerNotSupported } from '../../utils/dialogValidator'; @@ -30,13 +23,6 @@ import { TreeItem } from './treeItem'; // -------------------- Styles -------------------- // -const groupListStyle: Partial = { - root: { - width: '100%', - boxSizing: 'border-box', - }, -}; - const searchBox: ISearchBoxStyles = { root: { borderBottom: '1px solid #edebe9', @@ -53,32 +39,29 @@ const root = css` overflow-y: auto; overflow-x: hidden; .ms-List-cell { - min-height: 36px; + min-height: 24px; } `; +const summaryStyle = css` + display: flex; + padding-left: 12px; + padding-top: 12px; +`; + // -------------------- ProjectTree -------------------- // -function createGroupItem(dialog: DialogInfo, currentId: string, position: number, warningContent: string): IGroup { - return { - key: dialog.id, - name: dialog.displayName, - level: 1, - startIndex: position, - count: dialog.triggers.length, - hasMoreData: true, - isCollapsed: dialog.id !== currentId, - data: { ...dialog, warningContent }, - }; -} +export type TreeLink = { + displayName: string; + isRoot: boolean; + warningContent?: string; + projectId: string; + dialogName?: string; + trigger?: number; +}; -function createItem(trigger: ITrigger, index: number, warningContent: string) { - return { - ...trigger, - index, - warningContent, - displayName: trigger.displayName || getFriendlyName({ $kind: trigger.type }), - }; +function getTriggerName(trigger: ITrigger) { + return trigger.displayName || getFriendlyName({ $kind: trigger.type }); } function sortDialog(dialogs: DialogInfo[]) { @@ -94,91 +77,109 @@ function sortDialog(dialogs: DialogInfo[]) { }); } -function createItemsAndGroups( - dialogs: DialogInfo[], - dialogId: string, - filter: string -): { items: any[]; groups: IGroup[] } { - let position = 0; - const result = dialogs - .filter((dialog) => { - return dialog.displayName.toLowerCase().includes(filter.toLowerCase()); - }) - .reduce( - (result: { items: any[]; groups: IGroup[] }, dialog) => { - const warningContent = containUnsupportedTriggers(dialog); - result.groups.push(createGroupItem(dialog, dialogId, position, warningContent)); - position += dialog.triggers.length; - dialog.triggers.forEach((item, index) => { - const warningContent = triggerNotSupported(dialog, item); - result.items.push(createItem(item, index, warningContent)); - }); - return result; - }, - { items: [], groups: [] } - ); - return result; -} - interface IProjectTreeProps { - dialogs: DialogInfo[]; dialogId: string; selected: string; - onSelect: (id: string, selected?: string) => void; - onDeleteTrigger: (id: string, index: number) => void; - onDeleteDialog: (id: string) => void; + onSelect: (link: TreeLink) => void; + onDelete: (link: TreeLink) => void; } +const TYPE_TO_ICON_MAP = { + 'Microsoft.OnUnknownIntent': '', +}; + +type BotInProject = { + dialogs: DialogInfo[]; + projectId: string; + name: string; +}; + export const ProjectTree: React.FC = (props) => { const { onboardingAddCoachMarkRef, updateUserSettings } = useRecoilValue(dispatcherState); const { dialogNavWidth: currentWidth } = useRecoilValue(userSettingsState); - const groupRef: React.RefObject = useRef(null); - const { dialogs, dialogId, selected, onSelect, onDeleteTrigger, onDeleteDialog } = props; + const { selected, onSelect, onDelete } = props; const [filter, setFilter] = useState(''); const delayedSetFilter = debounce((newValue) => setFilter(newValue), 1000); const addMainDialogRef = useCallback((mainDialog) => onboardingAddCoachMarkRef({ mainDialog }), []); + const projectCollection = useRecoilValue(botProjectSpaceSelector); - const sortedDialogs = useMemo(() => { - return sortDialog(dialogs); - }, [dialogs]); + const renderBotHeader = (bot: BotInProject, hasWarnings: boolean) => { + const link: TreeLink = { + displayName: bot.name, + projectId: bot.projectId, + isRoot: true, + warningContent: hasWarnings ? formatMessage('This bot has warnings') : undefined, + }; - const onRenderHeader = (props: IGroupHeaderProps) => { - const toggleCollapse = (): void => { - groupRef.current?.toggleCollapseAll(true); - props.onToggleCollapse?.(props.group!); - onSelect(props.group!.key); + return ( + + + + ); + }; + + const renderDialogHeader = (projectId: string, dialog: DialogInfo, warningContent: string) => { + const link: TreeLink = { + dialogName: dialog.id, + displayName: dialog.displayName, + isRoot: dialog.isRoot, + projectId: projectId, + warningContent, }; return ( - + ); }; - function onRenderCell(nestingDepth?: number, item?: any): React.ReactNode { + function renderTrigger(projectId: string, item: any, dialog: DialogInfo): React.ReactNode { + const link: TreeLink = { + displayName: item.displayName, + warningContent: item.warningContent, + trigger: item.index, + dialogName: dialog.id, + isRoot: false, + projectId: projectId, + }; + return ( onDeleteTrigger(dialogId, item.index)} - onSelect={() => onSelect(dialogId, createSelectedPath(item.index))} + key={`${item.id}_${item.index}`} + depth={2} + dialogName={dialog.displayName} + extraSpace={32} + icon={TYPE_TO_ICON_MAP[item.type] || 'Flow'} + isActive={dialog.id === props.dialogId && createSelectedPath(item.index) === selected} + link={link} + onDelete={onDelete} + onSelect={onSelect} /> ); } - const onRenderShowAll = () => { - return null; - }; - const onFilter = (_e?: any, newValue?: string): void => { if (typeof newValue === 'string') { delayedSetFilter(newValue); @@ -189,68 +190,103 @@ export const ProjectTree: React.FC = (props) => { updateUserSettings({ dialogNavWidth: currentWidth + d.width }); }; - const itemsAndGroups: { items: any[]; groups: IGroup[] } = createItemsAndGroups(sortedDialogs, dialogId, filter); + function filterMatch(scope: string) { + return scope.toLowerCase().includes(filter.toLowerCase()); + } + + function createDetailsTree(bot: BotInProject) { + const { projectId } = bot; + const dialogs = sortDialog(bot.dialogs); + + const filteredDialogs = + filter == null || filter.length === 0 + ? dialogs + : dialogs.filter( + (dialog) => + filterMatch(dialog.displayName) || dialog.triggers.some((trigger) => filterMatch(getTriggerName(trigger))) + ); + + return filteredDialogs.map((dialog: DialogInfo) => { + const triggerList = dialog.triggers + .filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr))) + .map((tr, index) => + renderTrigger( + projectId, + { ...tr, index, displayName: getTriggerName(tr), warningContent: triggerNotSupported(dialog, tr) }, + dialog + ) + ); + return ( +
+ + {renderDialogHeader(projectId, dialog, containUnsupportedTriggers(dialog))} + + {triggerList} +
+ ); + }); + } + + function createBotSubtree(bot: BotInProject) { + return ( +
+ {renderBotHeader(bot, false)} + {createDetailsTree(bot)} +
+ ); + } + + const projectTree = + projectCollection.length === 1 ? createDetailsTree(projectCollection[0]) : projectCollection.map(createBotSubtree); return ( - -
+ - - -
+ + +
- - } - styles={groupListStyle} - onRenderCell={onRenderCell} - /> - -
- + { dialogNum: projectCollection.length } + )} + aria-live={'polite'} + /> + {projectTree} +
+
+
+ ); }; diff --git a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx index fc32818c8b..9b7de9ae4b 100644 --- a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx +++ b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx @@ -15,8 +15,10 @@ import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuStyles } from 'office-ui-fabric-react/lib/ContextualMenu'; import { ICalloutContentStyles } from 'office-ui-fabric-react/lib/Callout'; +import { TreeLink } from './ProjectTree'; + // -------------------- Styles -------------------- // -const indent = 16; +const indent = 8; const itemText = (depth: number) => css` outline: none; :focus { @@ -38,13 +40,15 @@ const content = css` outline: none; display: flex; align-items: center; + justify-items: center; + height: 24px; label: ProjectTreeItem; `; -const leftIndent = css` +const leftIndent = (extraSpace: number) => css` height: 100%; - width: ${indent}px; + width: ${extraSpace + 8}px; `; const moreMenu: Partial = { @@ -77,13 +81,13 @@ const moreButton = (isActive: boolean): IButtonStyles => { }; }; -const navItem = (isActive: boolean, isSubItemActive: boolean) => css` +const navItem = (isActive: boolean) => css` width: 100%; position: relative; - height: 36px; + height: 24px; font-size: 12px; - color: #545454; - background: ${isActive && !isSubItemActive ? '#f2f2f2' : 'transparent'}; + color: ${isActive ? '#ffffff' : '#545454'}; + background: ${isActive ? '#0078d4' : 'transparent'}; font-weight: ${isActive ? FontWeights.semibold : FontWeights.regular}; &:hover { color: #545454; @@ -110,13 +114,13 @@ const navItem = (isActive: boolean, isSubItemActive: boolean) => css` } `; -export const overflowSet = css` +export const overflowSet = (depth: number) => css` width: 100%; height: 100%; - padding-left: 12px; + padding-left: ${depth * 12}px; padding-right: 12px; box-sizing: border-box; - line-height: 36px; + line-height: 24px; justify-content: space-between; display: flex; justify-content: space-between; @@ -131,38 +135,43 @@ const warningIcon = { // -------------------- TreeItem -------------------- // interface ITreeItemProps { - link: any; - isActive: boolean; + link: TreeLink; + isActive?: boolean; isSubItemActive?: boolean; depth: number | undefined; - onDelete: (id: string) => void; - onSelect: (id: string) => void; + onDelete?: (link: TreeLink) => void; + onSelect: (link: TreeLink) => void; + icon?: string; + dialogName?: string; + showProps?: boolean; + extraSpace?: number; } -const onRenderItem = (item: IOverflowSetItemProps) => { +const onRenderItem = (extraSpace: number) => (item: IOverflowSetItemProps) => { const warningContent = formatMessage( 'This trigger type is not supported by the RegEx recognizer and will not be fired.' ); return (
-
+
{item.warningContent ? ( ) : ( -
+
)} - {item.depth !== 0 && ( + {item.icon != null && ( { ); }; -const onRenderOverflowButton = (isRoot: boolean, isActive: boolean) => { +const onRenderOverflowButton = (showIcon: boolean, isActive: boolean) => { const moreLabel = formatMessage('Actions'); - const showIcon = !isRoot; return (overflowItems) => { return showIcon ? ( @@ -205,18 +213,43 @@ const onRenderOverflowButton = (isRoot: boolean, isActive: boolean) => { }; export const TreeItem: React.FC = (props) => { - const { link, isActive, isSubItemActive, depth, onDelete, onSelect } = props; + const { link, isActive, depth, onDelete, onSelect, icon, dialogName } = props; + + const a11yLabel = `${dialogName ?? '$Root'}_${link.displayName}`; + + const overflowMenu: { key: string; name: string; onClick: () => void }[] = []; + + if (onDelete != null) { + overflowMenu.push({ + key: 'delete', + name: formatMessage('Delete'), + onClick: () => onDelete(link), + }); + } + + if (props.showProps) { + overflowMenu.push({ + key: 'props', + name: formatMessage('Properties'), + onClick: () => onSelect(link), + }); + } + + const linkString = `${link.projectId}_DialogTreeItem${link.dialogName}_${link.trigger ?? ''}`; return (
{ - onSelect(link.id); + onSelect(link); }} onKeyDown={(e) => { if (e.key === 'Enter') { - onSelect(link.id); + onSelect(link); } }} > @@ -224,26 +257,21 @@ export const TreeItem: React.FC = (props) => { //In 8.0 the OverflowSet will no longer be wrapped in a FocusZone //remove this at that time doNotContainWithinFocusZone - css={overflowSet} - data-testid={`DialogTreeItem${link.id}`} + css={overflowSet(depth ?? 0)} + data-testid={linkString} items={[ { - key: link.id, + key: linkString, depth, + icon, ...link, }, ]} - overflowItems={[ - { - key: 'delete', - name: formatMessage('Delete'), - onClick: () => onDelete(link.id), - }, - ]} + overflowItems={overflowMenu} role="row" styles={{ item: { flex: 1 } }} - onRenderItem={onRenderItem} - onRenderOverflowButton={onRenderOverflowButton(link.isRoot, isActive)} + onRenderItem={onRenderItem(props.extraSpace ?? 0)} + onRenderOverflowButton={onRenderOverflowButton(!link.isRoot, !!isActive)} />
); diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 6b73658839..a35580df88 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -157,8 +157,8 @@ export const extensionsState = atom({ default: [], }); -export const botProjectSpaceProjectIds = atom({ - key: getFullyQualifiedKey('botProjectsSpace'), +export const botProjectIdsState = atom({ + key: getFullyQualifiedKey('botProjectIdsState'), default: [], }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 1dcead710a..76d0fbc86b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -4,6 +4,7 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import formatMessage from 'format-message'; +import isNumber from 'lodash/isNumber'; import httpClient from '../../utils/httpUtil'; import { BotStatus } from '../../constants'; @@ -12,23 +13,28 @@ import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import settingStorage from '../../utils/dialogSettingStorage'; import { navigateTo } from '../../utils/navigation'; import { projectIdCache } from '../../utils/projectCache'; -import { botProjectSpaceProjectIds, currentProjectIdState, botStatusState } from '../atoms'; +import { + botProjectIdsState, + currentProjectIdState, + botStatusState, + botOpeningState, + projectMetaDataState, +} from '../atoms'; import { recentProjectsState, templateIdState, announcementState, boilerplateVersionState } from './../atoms'; import { logMessage, setError } from './../dispatchers/shared'; import { - fetchProjectDataById, - fetchProjectDataByPath, flushExistingTasks, handleProjectFailure, navigateToBot, openLocalSkill, initBotState, removeRecentProject, - openRootBotAndSkills, createNewBotFromTemplate, resetBotStates, openRemoteSkill, + openRootBotAndSkillsByProjectId, + openRootBotAndSkillsByPath, } from './utils/project'; export const projectDispatcher = () => { @@ -36,7 +42,8 @@ export const projectDispatcher = () => { (callbackHelpers: CallbackInterface) => async (projectId: string) => { try { const { set } = callbackHelpers; - set(botProjectSpaceProjectIds, (currentProjects) => currentProjects.filter((id) => id !== projectId)); + set(botProjectIdsState, (currentProjects) => currentProjects.filter((id) => id !== projectId)); + resetBotStates(callbackHelpers, projectId); } catch (ex) { setError(callbackHelpers, ex); } @@ -44,16 +51,37 @@ export const projectDispatcher = () => { ); const addExistingSkillToBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { - const projectId = await openLocalSkill(callbackHelpers, path, storageId); - return projectId; + (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default', pushIndex: number) => { + const { set } = callbackHelpers; + try { + set(botOpeningState, true); + const { projectId } = await openLocalSkill(callbackHelpers, path, storageId); + if (isNumber(pushIndex)) { + set(botProjectIdsState, (current: string[]) => { + const mutated = [...current]; + mutated.splice(pushIndex, 0, projectId); + return mutated; + }); + } else { + set(botProjectIdsState, (current) => [...current, projectId]); + } + return projectId; + } finally { + set(botOpeningState, false); + } } ); const addRemoteSkillToBotProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { - const projectId = await openRemoteSkill(callbackHelpers, manifestUrl, name); - return projectId; + const { set } = callbackHelpers; + try { + set(botOpeningState, true); + const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl, name); + return projectId; + } finally { + set(botOpeningState, false); + } } ); @@ -69,6 +97,7 @@ export const projectDispatcher = () => { ) => { const { set } = callbackHelpers; try { + set(botOpeningState, true); const { projectId, mainDialog } = await createNewBotFromTemplate( callbackHelpers, templateId, @@ -78,12 +107,18 @@ export const projectDispatcher = () => { schemaUrl, locale ); - set(botProjectSpaceProjectIds, (current) => [...current, projectId]); + set(projectMetaDataState(projectId), { + isRemote: false, + isRootBot: false, + }); + set(botProjectIdsState, (current) => [...current, projectId]); navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); return projectId; } catch (ex) { - set(botProjectSpaceProjectIds, []); + set(botProjectIdsState, []); handleProjectFailure(callbackHelpers, ex); + } finally { + set(botOpeningState, false); } } ); @@ -93,26 +128,36 @@ export const projectDispatcher = () => { const { set } = callbackHelpers; try { await flushExistingTasks(callbackHelpers); - const { projectData, botFiles } = await fetchProjectDataByPath(path, storageId); - openRootBotAndSkills(callbackHelpers, projectData, botFiles, 'default'); + set(botOpeningState, true); + set(botProjectIdsState, []); + const { projectId, mainDialog } = await openRootBotAndSkillsByPath(callbackHelpers, path, storageId); + projectIdCache.set(projectId); + navigateToBot(projectId, mainDialog); } catch (ex) { - set(botProjectSpaceProjectIds, []); + set(botProjectIdsState, []); + projectIdCache.clear(); removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); + } finally { + set(botOpeningState, false); } } ); const fetchProjectById = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { + const { set } = callbackHelpers; try { - const { set } = callbackHelpers; await flushExistingTasks(callbackHelpers); - set(botProjectSpaceProjectIds, []); - const { projectData, botFiles } = await fetchProjectDataById(projectId); - openRootBotAndSkills(callbackHelpers, projectData, botFiles, 'default'); + set(botOpeningState, true); + set(botProjectIdsState, []); + await openRootBotAndSkillsByProjectId(callbackHelpers, projectId); } catch (ex) { + set(botProjectIdsState, []); + projectIdCache.clear(); handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); + } finally { + set(botOpeningState, false); } }); @@ -128,6 +173,9 @@ export const projectDispatcher = () => { ) => { const { set } = callbackHelpers; try { + await flushExistingTasks(callbackHelpers); + set(botOpeningState, true); + set(botProjectIdsState, []); const { projectId, mainDialog } = await createNewBotFromTemplate( callbackHelpers, templateId, @@ -138,15 +186,18 @@ export const projectDispatcher = () => { locale ); projectIdCache.set(projectId); - set(botProjectSpaceProjectIds, [projectId]); + set(botProjectIdsState, [projectId]); set(currentProjectIdState, projectId); navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); return projectId; } catch (ex) { - set(botProjectSpaceProjectIds, []); + set(botProjectIdsState, []); + projectIdCache.clear(); removeRecentProject(callbackHelpers, location); handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); + } finally { + set(botOpeningState, false); } } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 0e4a369334..4e74f274ab 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - import path from 'path'; import { v4 as uuid } from 'uuid'; @@ -28,7 +27,7 @@ import { botDiagnosticsState, botProjectFileState, botProjectSpaceLoadedState, - botProjectSpaceProjectIds, + botProjectIdsState, currentProjectIdState, filePersistenceState, projectMetaDataState, @@ -77,7 +76,12 @@ export const resetBotStates = async ({ snapshot, gotoSnapshot }: CallbackInterfa gotoSnapshot(newSnapshot); }; -export const setErrorBotProject = async (callbackHelpers: CallbackInterface, projectId, botName, payload) => { +export const setErrorOnBotProject = async ( + callbackHelpers: CallbackInterface, + projectId: string, + botName: string, + payload: any +) => { const { set } = callbackHelpers; if (payload?.response?.data?.message) { set(botErrorState(projectId), payload.response.data); @@ -89,13 +93,13 @@ export const setErrorBotProject = async (callbackHelpers: CallbackInterface, pro export const flushExistingTasks = async (callbackHelpers) => { const { snapshot, reset } = callbackHelpers; - const projectIds = await snapshot.getPromise(botProjectSpaceProjectIds); + const projectIds = await snapshot.getPromise(botProjectIdsState); const recoilTasks: any[] = []; for (const projectId of projectIds) { const resetStates = await resetBotStates(callbackHelpers, projectId); recoilTasks.push(resetStates); } - reset(botProjectSpaceProjectIds); + reset(botProjectIdsState); const workers = [lgWorker, luWorker, qnaWorker]; return Promise.all([workers.map((w) => w.flush()), ...recoilTasks]); @@ -172,21 +176,7 @@ export const fetchProjectDataByPath = async ( export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { try { const response = await httpClient.get(`/projects/${projectId}`); - const projectData = loadProjectData(response); - return projectData; - } catch (ex) { - return { - botFiles: undefined, - projectData: undefined, - error: ex, - }; - } -}; - -export const createNewProject = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { - try { - const response = await httpClient.get(`/projects/${projectId}`); - const projectData = loadProjectData(response); + const { botFiles, projectData } = loadProjectData(response); return projectData; } catch (ex) { return { @@ -268,7 +258,7 @@ export const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialo return updateQnaFilesStatus(projectId, qnaFiles); }; -export const initBotState = async (callbackHelpers, data: any, botFiles, isRootBot = false) => { +export const initBotState = async (callbackHelpers: CallbackInterface, data: any, botFiles: any) => { const { snapshot, set } = callbackHelpers; const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; @@ -316,9 +306,6 @@ export const initBotState = async (callbackHelpers, data: any, botFiles, isRootB set(settingsState(projectId), mergedSettings); set(filePersistenceState(projectId), new FilePersistence(projectId)); set(undoHistoryState(projectId), new UndoHistory(projectId)); - set(projectMetaDataState(projectId), { - isRootBot, - }); return mainDialog; }; @@ -353,12 +340,12 @@ export const openRemoteSkill = async (callbackHelpers: CallbackInterface, manife }); set(botNameState(projectId), manifestResponse.data.name); set(locationState(projectId), manifestUrl); - return projectId; + return { projectId }; } catch (ex) { const tempProjectId = uuid(); set(botNameState(tempProjectId), name); - setErrorBotProject(callbackHelpers, tempProjectId, name, ex); - return tempProjectId; + setErrorOnBotProject(callbackHelpers, tempProjectId, name, ex); + return { projectId: tempProjectId }; } }; @@ -366,16 +353,19 @@ export const openLocalSkill = async (callbackHelpers, pathToBot: string, storage const { set } = callbackHelpers; const { projectData, botFiles, error } = await fetchProjectDataByPath(pathToBot, storageId); if (!error) { - await initBotState(callbackHelpers, projectData, botFiles); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); set(projectMetaDataState(projectData.id), { isRootBot: false, isRemote: false, }); - return projectData.id; + return { + projectId: projectData.id, + mainDialog, + }; } else { const tempProjectId = uuid(); set(botNameState(tempProjectId), name); - setErrorBotProject(callbackHelpers, tempProjectId, name, error); + setErrorOnBotProject(callbackHelpers, tempProjectId, name, error); return tempProjectId; } }; @@ -398,18 +388,17 @@ export const createNewBotFromTemplate = async ( schemaUrl, locale, }); - const { set } = callbackHelpers; + const { botFiles, projectData } = loadProjectData(response); const projectId = response.data.id; if (settingStorage.get(projectId)) { settingStorage.remove(projectId); } - const mainDialog = await initBotState(callbackHelpers, response.data, true); - set(botProjectSpaceLoadedState, true); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); return { projectId, mainDialog }; }; const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => { - set(botProjectSpaceProjectIds, (current: string[]) => { + set(botProjectIdsState, (current: string[]) => { const botProjectIDs = [...current, projectId]; if (botProjectIDs.length === skillCt) { set(botProjectSpaceLoadedState, true); @@ -418,11 +407,13 @@ const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => }); }; -export const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, projectData, botFiles, storageId) => { +const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { + const { projectData, botFiles } = data; const { set } = callbackHelpers; - set(botOpeningState, true); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); const rootBotProjectId = projectData.id; + if (botFiles.botProjectSpaceFiles.length) { const currentBotProjectFile: BotProjectSpace = botFiles.botProjectSpaceFiles[0]; set(botProjectFileState(rootBotProjectId), currentBotProjectFile); @@ -433,17 +424,14 @@ export const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, p if (!skillInBotProject.remote) { const path = parseFileProtocolPaths(currentBotProjectFile.workspace, skillInBotProject.workspace); //TODO handle exception - openLocalSkill(callbackHelpers, path, skillInBotProject.name, storageId).then((projectId: string) => - addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length) + openLocalSkill(callbackHelpers, path, storageId, skillInBotProject.name).then((result) => + addProjectToBotProjectSpace(set, result.projectId, skillsInBotProject.length) ); } else { - //TODO handle exception if (skillInBotProject.manifest) { - openRemoteSkill( - callbackHelpers, - skillInBotProject.manifest, - skillInBotProject.name - ).then((projectId: string) => addProjectToBotProjectSpace(set, projectId, skillsInBotProject.length)); + openRemoteSkill(callbackHelpers, skillInBotProject.manifest, skillInBotProject.name).then((result) => + addProjectToBotProjectSpace(set, result.projectId, skillsInBotProject.length) + ); } } } @@ -451,9 +439,23 @@ export const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, p } else { set(botProjectSpaceLoadedState, true); } - //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject - set(botProjectSpaceProjectIds, [rootBotProjectId]); + set(botProjectIdsState, [rootBotProjectId]); set(botOpeningState, false); set(currentProjectIdState, rootBotProjectId); - navigateToBot(rootBotProjectId, mainDialog); + return { + mainDialog, + projectId: rootBotProjectId, + }; +}; + +export const openRootBotAndSkillsByPath = async (callbackHelpers: CallbackInterface, path: string, storageId) => { + const data = await fetchProjectDataByPath(path, storageId); + const result = await openRootBotAndSkills(callbackHelpers, data, storageId); + return result; +}; + +export const openRootBotAndSkillsByProjectId = async (callbackHelpers: CallbackInterface, projectId: string) => { + const data = await fetchProjectDataById(projectId); + const result = await openRootBotAndSkills(callbackHelpers, data); + return result; }; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index 2cf37a16d3..a5869ff077 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -5,7 +5,7 @@ import { selector } from 'recoil'; import { dispatcherState } from '../DispatcherWrapper'; import { Dispatcher } from '../dispatchers'; -import { botErrorState, botNameState, botProjectSpaceProjectIds, dialogsState, projectMetaDataState } from '../atoms'; +import { botErrorState, botNameState, botProjectIdsState, dialogsState, projectMetaDataState } from '../atoms'; // Actions const projectLoadAction = (dispatcher: Dispatcher) => { @@ -59,6 +59,10 @@ const projectLoadAction = (dispatcher: Dispatcher) => { const skillId = await dispatcher.addRemoteSkillToBotProject(manifestUrl, name, endpointName); await dispatcher.addSkillToBotProject(rootBotId, skillId, true); }, + replaceSkillAtIndex: async (projectId: string, index: number, newPath: string, storageId = 'default') => { + await dispatcher.removeSkillFromBotProject(projectId); + await dispatcher.addExistingSkillToBotProject(newPath, storageId, index); + }, }; }; @@ -76,8 +80,8 @@ export const projectLoadSelector = selector({ export const botProjectsWithoutErrorsSelector = selector({ key: 'botProjectsWithoutErrorsSelector', get: ({ get }) => { - const botProjectIds = get(botProjectSpaceProjectIds); - return botProjectIds + const botProjectIds = get(botProjectIdsState); + const projectsWithoutErrors = botProjectIds .filter((projectId) => !get(botErrorState(projectId))) .map((projectId: string) => { const metaData = get(projectMetaDataState(projectId)); @@ -86,13 +90,14 @@ export const botProjectsWithoutErrorsSelector = selector({ ...metaData, }; }); + return projectsWithoutErrors; }, }); export const botProjectSpaceSelector = selector({ key: 'botProjectSpaceSelector', get: ({ get }) => { - const botProjects = get(botProjectSpaceProjectIds); + const botProjects = get(botProjectIdsState); const result = botProjects.map((projectId: string) => { const dialogs = get(dialogsState(projectId)); const metaData = get(projectMetaDataState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx index 6f046da8ad..b1ce067af0 100644 --- a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx +++ b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx @@ -13,14 +13,14 @@ import { luFilesState, projectMetaDataState, currentProjectIdState, - botProjectSpaceProjectIds, + botProjectIdsState, } from '../../atoms'; import { renderRecoilHook } from '../../../../__tests__/testUtils/react-recoil-hooks-testing-library'; import UndoHistory from '../undoHistory'; const projectId = '123-asd'; export const UndoRedoWrapper = () => { - const botProjects = useRecoilValue(botProjectSpaceProjectIds); + const botProjects = useRecoilValue(botProjectIdsState); return botProjects.length > 0 ? : null; }; @@ -59,7 +59,7 @@ describe('', () => { ); }, states: [ - { recoilState: botProjectSpaceProjectIds, initialValue: [projectId] }, + { recoilState: botProjectIdsState, initialValue: [projectId] }, { recoilState: dialogsState(projectId), initialValue: [{ id: '1' }] }, { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] }, { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] }, diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index 547af8a8d9..7ed944c003 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -15,7 +15,7 @@ import { BASEPATH } from './constants'; import { dispatcherState, schemasState, - botProjectSpaceProjectIds, + botProjectIdsState, botProjectSpaceLoadedState, botOpeningState, } from './recoilModel'; @@ -95,7 +95,7 @@ const ProjectRouter: React.FC> = (pro const { projectId = '' } = props; const schemas = useRecoilValue(schemasState(projectId)); const { fetchProjectById } = useRecoilValue(dispatcherState); - const botProjects = useRecoilValue(botProjectSpaceProjectIds); + const botProjects = useRecoilValue(botProjectIdsState); const botsLoaded = useRecoilValue(botProjectSpaceLoadedState); useEffect(() => { From cb0c67ea450008a0c464fa38637e16ff71adce7c Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sat, 26 Sep 2020 19:43:30 -0700 Subject: [PATCH 012/100] Bot Project file creation, adding remote/local skills. Populate state management with rootbot and skills --- .../DefineConversation/index.test.tsx | 2 +- .../publish-luis-modal.test.tsx | 4 +- .../__tests__/components/appSettings.test.tsx | 2 + .../__tests__/components/design.test.tsx | 2 +- .../__tests__/components/projecttree.test.tsx | 2 +- .../__tests__/pages/publish/Publish.test.tsx | 4 +- .../components/CreationFlow/CreationFlow.tsx | 29 +- .../packages/client/src/components/Header.tsx | 10 +- .../components/ProjectTree/ProjectTree.tsx | 10 +- .../TestController/TestController.tsx | 4 +- .../client/src/pages/design/DesignPage.tsx | 74 +++- .../exportSkillModal/content/Description.tsx | 4 +- .../exportSkillModal/content/SaveManifest.tsx | 4 +- .../packages/client/src/pages/home/Home.tsx | 4 +- .../src/pages/knowledge-base/QnAPage.tsx | 4 +- .../pages/notifications/useNotifications.tsx | 3 + .../client/src/pages/publish/Publish.tsx | 4 +- .../client/src/pages/setting/SettingsPage.tsx | 2 +- .../dialog-settings/DialogSettings.tsx | 10 +- .../runtime-settings/RuntimeSettings.tsx | 4 +- .../client/src/pages/skills/index.tsx | 4 +- .../src/recoilModel/DispatcherWrapper.tsx | 36 +- .../client/src/recoilModel/atoms/botState.ts | 18 +- .../dispatchers/__tests__/export.test.tsx | 6 +- .../__tests__/mocks/mockProjectResponse.json | 6 + .../dispatchers/__tests__/project.test.tsx | 38 ++- .../recoilModel/dispatchers/botProjectFile.ts | 120 +++---- .../src/recoilModel/dispatchers/export.ts | 4 +- .../src/recoilModel/dispatchers/multilang.ts | 6 +- .../src/recoilModel/dispatchers/project.ts | 233 ++++++++----- .../src/recoilModel/dispatchers/shared.ts | 18 +- .../recoilModel/dispatchers/utils/project.ts | 322 ++++++++++++------ .../persistence/FilePersistence.ts | 23 +- .../src/recoilModel/persistence/types.ts | 1 + .../src/recoilModel/selectors/project.ts | 131 +++---- Composer/packages/client/src/router.tsx | 11 +- .../packages/client/src/shell/useShell.ts | 4 +- .../packages/client/src/utils/fileUtil.ts | 25 ++ .../indexers/src/botProjectSpaceIndexer.ts | 6 +- .../lib/shared/src/skillsUtils/index.ts | 5 + .../packages/lib/shared/src/types/indexers.ts | 13 +- .../server/src/controllers/project.ts | 7 - .../server/src/models/asset/assetManager.ts | 26 ++ .../server/src/models/bot/botProject.ts | 61 +++- .../server/src/models/bot/botStructure.ts | 4 + .../ActionsSample/actionssample.botproj | 6 + .../askingquestionssample.botproj | 6 + .../controllingconversationflowsample.botproj | 6 + .../assets/projects/EchoBot/echobot.botproj | 6 + .../assets/projects/EmptyBot/emptybot.botproj | 6 + .../interruptionssample.botproj | 6 + .../qnamakerluissample.botproj | 6 + .../projects/QnASample/qnasample.botproj | 6 + .../respondingwithcardssample.botproj | 6 + .../respondingwithtextsample.botproj | 6 + .../todobotwithluissample.botproj | 6 + .../projects/TodoSample/todosample.botproj | 6 + 57 files changed, 932 insertions(+), 450 deletions(-) create mode 100644 Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj create mode 100644 Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj create mode 100644 Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj create mode 100644 Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj create mode 100644 Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj create mode 100644 Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj create mode 100644 Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj create mode 100644 Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj create mode 100644 Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj create mode 100644 Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj create mode 100644 Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj create mode 100644 Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj diff --git a/Composer/packages/client/__tests__/components/CreationFlow/DefineConversation/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlow/DefineConversation/index.test.tsx index 40dfbd0912..edebf38a00 100644 --- a/Composer/packages/client/__tests__/components/CreationFlow/DefineConversation/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlow/DefineConversation/index.test.tsx @@ -45,7 +45,7 @@ describe('', () => { onSubmit={onSubmitMock} />, ({ set }) => { - set(focusedStorageFolderState, ''); + set(focusedStorageFolderState, {} as StorageFolder); set(storagesState, [{ id: 'default' }]); } ); diff --git a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx index 3bb6f18748..ee850212a2 100644 --- a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx +++ b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { fireEvent } from '@bfc/test-utils'; import { PublishDialog } from '../../../src/components/TestController/publishDialog'; -import { botNameState, settingsState, dispatcherState, currentProjectIdState } from '../../../src/recoilModel'; +import { botDisplayNameState, settingsState, dispatcherState, currentProjectIdState } from '../../../src/recoilModel'; import { renderWithRecoil } from '../../testUtils'; jest.useFakeTimers(); @@ -31,7 +31,7 @@ describe('', () => { setSettings: setSettingsMock, }); set(currentProjectIdState, projectId); - set(botNameState(projectId), 'sampleBot0'); + set(botDisplayNameState(projectId), 'sampleBot0'); set(settingsState(projectId), { luis: luisConfig, qna: qnaConfig, diff --git a/Composer/packages/client/__tests__/components/appSettings.test.tsx b/Composer/packages/client/__tests__/components/appSettings.test.tsx index ee9816e3a1..b966bd0324 100644 --- a/Composer/packages/client/__tests__/components/appSettings.test.tsx +++ b/Composer/packages/client/__tests__/components/appSettings.test.tsx @@ -21,6 +21,7 @@ describe(' & ', () => { const { getByText, getAllByText } = renderWithRecoil(, ({ set }) => { set(onboardingState, { complete: false, + coachMarkRefs: {}, }); }); // there are 2 onboarding texts @@ -43,6 +44,7 @@ describe(' & ', () => { }, propertyEditorWidth: 400, dialogNavWidth: 180, + appLocale: 'en-us', }); }); getByText('Application Updates'); diff --git a/Composer/packages/client/__tests__/components/design.test.tsx b/Composer/packages/client/__tests__/components/design.test.tsx index a93bab9484..3231e33d7a 100644 --- a/Composer/packages/client/__tests__/components/design.test.tsx +++ b/Composer/packages/client/__tests__/components/design.test.tsx @@ -7,7 +7,7 @@ import { DialogInfo } from '@bfc/shared'; import { renderWithRecoil } from '../testUtils'; import { dialogs } from '../constants.json'; -import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree-stable'; +import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree'; import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal'; import { CreateDialogModal } from '../../src/pages/design/createDialogModal'; diff --git a/Composer/packages/client/__tests__/components/projecttree.test.tsx b/Composer/packages/client/__tests__/components/projecttree.test.tsx index 9e08b50523..217e1743b5 100644 --- a/Composer/packages/client/__tests__/components/projecttree.test.tsx +++ b/Composer/packages/client/__tests__/components/projecttree.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { fireEvent } from '@bfc/test-utils'; import { dialogs } from '../constants.json'; -import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree-stable'; +import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree'; import { renderWithRecoil } from '../testUtils'; describe('', () => { diff --git a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx index b9aa12ddfe..5d498305a3 100644 --- a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx +++ b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { renderWithRecoil } from '../../testUtils'; import { settingsState, - botNameState, + botDisplayNameState, publishTypesState, publishHistoryState, currentProjectIdState, @@ -53,7 +53,7 @@ const state = { const initRecoilState = ({ set }) => { set(currentProjectIdState, state.projectId); - set(botNameState(state.projectId), state.botName); + set(botDisplayNameState(state.projectId), state.botName); set(publishTypesState(state.projectId), state.publishTypes); set(publishHistoryState(state.projectId), state.publishHistory); set(settingsState(state.projectId), state.settings); diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index 3c01b9b992..72ceec9211 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -22,7 +22,6 @@ import Home from '../../pages/home/Home'; import ImportQnAFromUrlModal from '../../pages/knowledge-base/ImportQnAFromUrlModal'; import { QnABotTemplateId } from '../../constants'; import { useProjectIdCache } from '../../utils/hooks'; -import { projectLoadSelector } from '../../recoilModel/selectors/project'; import { CreateOptions } from './CreateOptions'; import { OpenProject } from './OpenProject'; @@ -41,9 +40,12 @@ const CreationFlow: React.FC = () => { updateFolder, saveTemplateId, fetchRecentProjects, + openProject, + createNewBot, + saveProjectAs, + fetchProjectById, } = useRecoilValue(dispatcherState); - const projectLoader = useRecoilValue(projectLoadSelector); const creationFlowStatus = useRecoilValue(creationFlowStatusState); const projectId = useRecoilValue(currentProjectIdState); const templateProjects = useRecoilValue(templateProjectsState); @@ -69,7 +71,7 @@ const CreationFlow: React.FC = () => { // fetchProject use `gotoSnapshot` which will wipe out all state value. // so here make those methods call in sequence. if (!projectId && cachedProjectId) { - await projectLoader.fetchProjectById(cachedProjectId); + await fetchProjectById(cachedProjectId); } await fetchStorages(); fetchTemplates(); @@ -97,23 +99,24 @@ const CreationFlow: React.FC = () => { const openBot = async (botFolder) => { setCreationFlowStatus(CreationFlowStatus.CLOSE); - projectLoader.openProject(botFolder); + openProject(botFolder); }; const handleCreateNew = async (formData, templateId: string, qnaKbUrls?: string[]) => { - projectLoader.createProject( - templateId || '', - formData.name, - formData.description, - formData.location, - formData.schemaUrl, + const newBotData = { + templateId: templateId || '', + name: formData.name, + description: formData.description, + location: formData.location, + schemaUrl: formData.schemaUrl, appLocale, - qnaKbUrls - ); + qnaKbUrls, + }; + createNewBot(newBotData); }; const handleSaveAs = (formData) => { - projectLoader.saveProjectAs(projectId, formData.name, formData.description, formData.location); + saveProjectAs(projectId, formData.name, formData.description, formData.location); }; const handleCreateQnA = async (urls: string[]) => { diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index 98519ed5a6..c4bc3796e9 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -10,7 +10,13 @@ import { useRecoilValue } from 'recoil'; import { SharedColors } from '@uifabric/fluent-theme'; import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; -import { dispatcherState, appUpdateState, botNameState, localeState, currentProjectIdState } from '../recoilModel'; +import { + dispatcherState, + appUpdateState, + botDisplayNameState, + localeState, + currentProjectIdState, +} from '../recoilModel'; import composerIcon from '../images/composerIcon.svg'; import { AppUpdaterStatus } from '../constants'; @@ -75,7 +81,7 @@ const headerTextContainer = css` export const Header = () => { const { setAppUpdateShowing } = useRecoilValue(dispatcherState); const projectId = useRecoilValue(currentProjectIdState); - const projectName = useRecoilValue(botNameState(projectId)); + const projectName = useRecoilValue(botDisplayNameState(projectId)); const locale = useRecoilValue(localeState(projectId)); const appUpdate = useRecoilValue(appUpdateState); const { showing, status } = appUpdate; diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index d51dc20a0a..7ba56d52d5 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -79,9 +79,11 @@ function sortDialog(dialogs: DialogInfo[]) { interface IProjectTreeProps { dialogId: string; - selected: string; + selected: any; + dialogs: DialogInfo[]; + onDeleteDialog: any; + onDeleteTrigger: any; onSelect: (link: TreeLink) => void; - onDelete: (link: TreeLink) => void; } const TYPE_TO_ICON_MAP = { @@ -89,7 +91,7 @@ const TYPE_TO_ICON_MAP = { }; type BotInProject = { - dialogs: DialogInfo[]; + dialogs: any; projectId: string; name: string; }; @@ -98,7 +100,7 @@ export const ProjectTree: React.FC = (props) => { const { onboardingAddCoachMarkRef, updateUserSettings } = useRecoilValue(dispatcherState); const { dialogNavWidth: currentWidth } = useRecoilValue(userSettingsState); - const { selected, onSelect, onDelete } = props; + const { selected, onSelect, onDeleteDialog: onDelete } = props; const [filter, setFilter] = useState(''); const delayedSetFilter = debounce((newValue) => setFilter(newValue), 1000); const addMainDialogRef = useCallback((mainDialog) => onboardingAddCoachMarkRef({ mainDialog }), []); diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index 1866dd6d0f..27549b7274 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -15,7 +15,7 @@ import { dispatcherState, validateDialogSelectorFamily, botStatusState, - botNameState, + botDisplayNameState, luFilesState, qnaFilesState, settingsState, @@ -62,7 +62,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); const botStatus = useRecoilValue(botStatusState(projectId)); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index d4dedb2b5c..f3563ed189 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -110,7 +110,6 @@ const DesignPage: React.FC { + addRemoteSkillToBotProject( + 'https://onenote-dev.azurewebsites.net/manifests/OneNoteSync-2-1-preview-1-manifest.json', + 'OneNoteSyncer', + 'remote' + ); + }, + }, + { + 'data-testid': 'AddLocalSkill', + key: 'addLocalSkill', + text: formatMessage(`Add local skill from path`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + addExistingSkillToBotProject('/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync'); + }, + }, + { + 'data-testid': 'createNewSkill', + key: 'createNewSkill', + text: formatMessage(`Create new Skill`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + addNewSkillToBotProject({ + name: 'newers-bot', + description: '', + schemaUrl: '', + location: '/Users/srravich/Desktop/samples', + templateId: 'InterruptionSample', + locale: appLocale, + qnaKbUrls: [], + }); + }, + }, + { + 'data-testid': 'removeSkillAtIndex', + key: 'removeSkillAtIndex', + text: formatMessage(`Remove a skill`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + const matchedProject: any = botProjectsSpace[botProjectsSpace.length - 1]; + removeSkillFromBotProject(matchedProject.projectId); + }, + }, + { + 'data-testid': 'replaceSkillAtIndex', + key: 'replaceSkillAtIndex', + text: formatMessage(`Replace a skill`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + const matchedProject: any = botProjectsSpace[botProjectsSpace.length - 1]; + replaceSkillInBotProject( + matchedProject.projectId, + '/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync' + ); + }, + }, ], }, }, diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx index 269a8fea45..a05a44f953 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx @@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil'; import { v4 as uuid } from 'uuid'; import { ContentProps } from '../constants'; -import { botNameState } from '../../../../recoilModel'; +import { botDisplayNameState } from '../../../../recoilModel'; const styles = { row: css` @@ -51,7 +51,7 @@ const InlineLabelField: React.FC = (props) => { }; export const Description: React.FC = ({ errors, value, schema, onChange, projectId }) => { - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const { $schema, ...rest } = value; const { hidden, properties } = useMemo( diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx index 38578069a7..41567316da 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx @@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; import { ContentProps, VERSION_REGEX } from '../constants'; -import { botNameState, skillManifestsState } from '../../../../recoilModel'; +import { botDisplayNameState, skillManifestsState } from '../../../../recoilModel'; const styles = { container: css` @@ -42,7 +42,7 @@ export const getManifestId = ( }; export const SaveManifest: React.FC = ({ errors, manifest, setSkillManifest, projectId }) => { - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const { id } = manifest; diff --git a/Composer/packages/client/src/pages/home/Home.tsx b/Composer/packages/client/src/pages/home/Home.tsx index 334bf548cc..09c02ed024 100644 --- a/Composer/packages/client/src/pages/home/Home.tsx +++ b/Composer/packages/client/src/pages/home/Home.tsx @@ -12,7 +12,7 @@ import { navigate } from '@reach/router'; import { useRecoilValue } from 'recoil'; import { CreationFlowStatus } from '../../constants'; -import { dispatcherState, botNameState } from '../../recoilModel'; +import { dispatcherState, botDisplayNameState } from '../../recoilModel'; import { recentProjectsState, templateProjectsState, @@ -63,7 +63,7 @@ const tutorials = [ const Home: React.FC = () => { const templateProjects = useRecoilValue(templateProjectsState); const projectId = useRecoilValue(currentProjectIdState); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const recentProjects = useRecoilValue(recentProjectsState); const templateId = useRecoilValue(templateIdState); const { openProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue( diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 5e38f9b824..8381f84378 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -15,7 +15,7 @@ import { navigateTo } from '../../utils/navigation'; import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; -import { botNameState, dialogsState, qnaAllUpViewStatusState } from '../../recoilModel/atoms/botState'; +import { botDisplayNameState, dialogsState, qnaAllUpViewStatusState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { QnAAllUpViewStatus } from '../../recoilModel/types'; @@ -34,7 +34,7 @@ const QnAPage: React.FC = (props) => { const actions = useRecoilValue(dispatcherState); const dialogs = useRecoilValue(dialogsState(projectId)); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); //To do: support other languages const locale = 'en-us'; //const locale = useRecoilValue(localeState); diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index bbe66112bd..d81c24781d 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -15,6 +15,7 @@ import { skillManifestsState, dialogSchemasState, qnaFilesState, + botProjectFileState, } from '../../recoilModel'; import { @@ -38,6 +39,7 @@ export default function useNotifications(projectId: string, filter?: string) { const skillManifests = useRecoilValue(skillManifestsState(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); + const botProjectFile = useRecoilValue(botProjectFileState(projectId)); const botAssets = { projectId, @@ -48,6 +50,7 @@ export default function useNotifications(projectId: string, filter?: string) { skillManifests, setting, dialogSchemas, + botProjectFile, }; const memoized = useMemo(() => { diff --git a/Composer/packages/client/src/pages/publish/Publish.tsx b/Composer/packages/client/src/pages/publish/Publish.tsx index 5edbfac6ed..9212549a96 100644 --- a/Composer/packages/client/src/pages/publish/Publish.tsx +++ b/Composer/packages/client/src/pages/publish/Publish.tsx @@ -16,7 +16,7 @@ import { projectContainer } from '../design/styles'; import { dispatcherState, settingsState, - botNameState, + botDisplayNameState, publishTypesState, publishHistoryState, } from '../../recoilModel'; @@ -35,7 +35,7 @@ const Publish: React.FC(); const settings = useRecoilValue(settingsState(projectId)); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const publishTypes = useRecoilValue(publishTypesState(projectId)); const publishHistory = useRecoilValue(publishHistoryState(projectId)); diff --git a/Composer/packages/client/src/pages/setting/SettingsPage.tsx b/Composer/packages/client/src/pages/setting/SettingsPage.tsx index 0ceee729f0..4bf6b3abfb 100644 --- a/Composer/packages/client/src/pages/setting/SettingsPage.tsx +++ b/Composer/packages/client/src/pages/setting/SettingsPage.tsx @@ -37,7 +37,7 @@ const getProjectLink = (path: string, id?: string) => { const SettingPage: React.FC = () => { const projectId = useRecoilValue(currentProjectIdState); const { - deleteBotProject, + deleteBot: deleteBotProject, addLanguageDialogBegin, addLanguageDialogCancel, delLanguageDialogBegin, diff --git a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx index d4c781d095..4af89f7a76 100644 --- a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx +++ b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx @@ -14,7 +14,13 @@ import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import cloneDeep from 'lodash/cloneDeep'; import { Label } from 'office-ui-fabric-react/lib/Label'; -import { dispatcherState, userSettingsState, botNameState, localeState, settingsState } from '../../../recoilModel'; +import { + dispatcherState, + userSettingsState, + botDisplayNameState, + localeState, + settingsState, +} from '../../../recoilModel'; import { languageListTemplates } from '../../../components/MultiLanguage'; import { settingsEditor, toolbar } from './style'; @@ -22,7 +28,7 @@ import { BotSettings } from './constants'; export const DialogSettings: React.FC> = (props) => { const { projectId = '' } = props; - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const locale = useRecoilValue(localeState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const userSettings = useRecoilValue(userSettingsState); diff --git a/Composer/packages/client/src/pages/setting/runtime-settings/RuntimeSettings.tsx b/Composer/packages/client/src/pages/setting/runtime-settings/RuntimeSettings.tsx index 8280f49d25..59557a652a 100644 --- a/Composer/packages/client/src/pages/setting/runtime-settings/RuntimeSettings.tsx +++ b/Composer/packages/client/src/pages/setting/runtime-settings/RuntimeSettings.tsx @@ -17,7 +17,7 @@ import { dispatcherState, ejectRuntimeSelector, boilerplateVersionState, - botNameState, + botDisplayNameState, settingsState, isEjectRuntimeExistState, } from '../../../recoilModel'; @@ -30,7 +30,7 @@ import { breathingSpace, runtimeSettingsStyle, runtimeControls, runtimeToggle, c export const RuntimeSettings: React.FC> = (props) => { const { projectId = '' } = props; - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const ejectedRuntimeExists = useRecoilValue(isEjectRuntimeExistState(projectId)); diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx index bdfa8467b9..12df39ccaf 100644 --- a/Composer/packages/client/src/pages/skills/index.tsx +++ b/Composer/packages/client/src/pages/skills/index.tsx @@ -9,7 +9,7 @@ import formatMessage from 'format-message'; import { useRecoilValue } from 'recoil'; import { SkillSetting } from '@bfc/shared'; -import { dispatcherState, settingsState, botNameState } from '../../recoilModel'; +import { dispatcherState, settingsState, botDisplayNameState } from '../../recoilModel'; import { Toolbar, IToolbarItem } from '../../components/Toolbar'; import { TestController } from '../../components/TestController/TestController'; import { CreateSkillModal } from '../../components/CreateSkillModal'; @@ -22,7 +22,7 @@ const Skills: React.FC> = (props) => const { projectId = '' } = props; const [showAddSkillDialogModal, setShowAddSkillDialogModal] = useState(false); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const { addSkill, setSettings } = useRecoilValue(dispatcherState); diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index e5ac938868..f1ec735e29 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -22,10 +22,9 @@ import { dialogSchemasState, settingsState, filePersistenceState, - projectMetaDataState, - botProjectSpaceLoadedState, + botProjectFileState, } from './atoms'; -import { botProjectsWithoutErrorsSelector } from './selectors'; +import { botsForFilePersistenceSelector } from './selectors'; const getBotAssets = async (projectId, snapshot: Snapshot): Promise => { const result = await Promise.all([ @@ -36,6 +35,7 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = snapshot.getPromise(skillManifestsState(projectId)), snapshot.getPromise(settingsState(projectId)), snapshot.getPromise(dialogSchemasState(projectId)), + snapshot.getPromise(botProjectFileState(projectId)), ]); return { projectId, @@ -46,6 +46,7 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = skillManifests: result[4], setting: result[5], dialogSchemas: result[6], + botProjectFile: result[7], }; }; @@ -87,32 +88,25 @@ const InitDispatcher = ({ onLoad }) => { export const DispatcherWrapper = ({ children }) => { const [loaded, setLoaded] = useState(false); - const botProjects = useRecoilValue(botProjectsWithoutErrorsSelector); - const botProjectLoaded = useRecoilValue(botProjectSpaceLoadedState); + const botProjects = useRecoilValue(botsForFilePersistenceSelector); useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => { - if (botProjectLoaded) { - for (const { projectId } of botProjects) { - const metaData = await snapshot.getPromise(projectMetaDataState(projectId)); - if (!metaData.isRemote) { - const assets = await getBotAssets(projectId, snapshot); - const previousAssets = await getBotAssets(projectId, previousSnapshot); - const filePersistence = await snapshot.getPromise(filePersistenceState(projectId)); - if (!isEmpty(filePersistence)) { - filePersistence.notify(assets, previousAssets); - } - } + const botsForFilePersistence = await snapshot.getPromise(botsForFilePersistenceSelector); + for (const projectId of botsForFilePersistence) { + const assets = await getBotAssets(projectId, snapshot); + const previousAssets = await getBotAssets(projectId, previousSnapshot); + const filePersistence = await snapshot.getPromise(filePersistenceState(projectId)); + if (!isEmpty(filePersistence)) { + filePersistence.notify(assets, previousAssets); } } }); return ( - {botProjects - .filter(({ isRemote }) => !isRemote) - .map(({ projectId }) => ( - - ))} + {botProjects.map((projectId) => ( + + ))} {loaded ? children : null} diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 047e5c01dd..dda741b498 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -13,6 +13,7 @@ import { Skill, DialogSetting, BotProjectSpace, + BotProjectFile, } from '@bfc/shared'; import { BotLoadError, DesignPageLocation, QnAAllUpViewStatus } from '../../recoilModel/types'; @@ -43,8 +44,8 @@ export const dialogSchemasState = atomFamily({ default: [], }); -export const botNameState = atomFamily({ - key: getFullyQualifiedKey('botName'), +export const botDisplayNameState = atomFamily({ + key: getFullyQualifiedKey('botDisplayName'), default: (id) => { return ''; }, @@ -259,9 +260,13 @@ export const filePersistenceState = atomFamily({ dangerouslyAllowMutability: true, }); -export const botProjectFileState = atomFamily({ +export const botProjectFileState = atomFamily({ key: getFullyQualifiedKey('botProjectFileState'), - default: {} as BotProjectSpace, + default: { + content: {} as BotProjectSpace, + id: '', + lastModified: '', + }, }); // Always using the first manifest file for now. We dont have the need to support multiple manifest for a skill @@ -274,3 +279,8 @@ export const botErrorState = atomFamily({ key: getFullyQualifiedKey('botError'), default: undefined, }); + +export const botNameIdentifierState = atomFamily({ + key: getFullyQualifiedKey('botNameIdentifierState'), + default: '', +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx index c02ff75ea7..0811086a2e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx @@ -7,7 +7,7 @@ import { act } from '@bfc/test-utils/lib/hooks'; import httpClient from '../../../utils/httpUtil'; import { exportDispatcher } from '../export'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; -import { botNameState, currentProjectIdState } from '../../atoms'; +import { botDisplayNameState, currentProjectIdState } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '../../../recoilModel/dispatchers'; @@ -22,7 +22,7 @@ describe('Export dispatcher', () => { prevAppendChild = document.body.appendChild; const useRecoilTestHook = () => { - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const currentDispatcher = useRecoilValue(dispatcherState); return { botName, @@ -33,7 +33,7 @@ describe('Export dispatcher', () => { const { result } = renderRecoilHook(useRecoilTestHook, { states: [ { recoilState: currentProjectIdState, initialValue: projectId }, - { recoilState: botNameState(projectId), initialValue: 'emptybot-1' }, + { recoilState: botDisplayNameState(projectId), initialValue: 'emptybot-1' }, ], dispatcher: { recoilState: dispatcherState, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json index 1f752cb915..0cdf902bb6 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json @@ -23,6 +23,12 @@ "path": "/Users/tester/Desktop/EmptyBot-1/dialogs/additem/language-understanding/en-us/additem.en-us.lu", "relativePath": "dialogs/additem/language-understanding/en-us/additem.en-us.lu", "lastModified": "Thu Jul 09 2020 10:19:09 GMT-0700 (Pacific Daylight Time)" + },{ + "name": "EmptyBot-1.botproj", + "content": "{\"$schema\":\"https:\/\/schemas.botframework.com\/schemas\/botprojects\/v0.1\/botproject-schema.json\",\"name\":\"echobot-0\",\"workspace\":\"\/Users\/srravich\/Desktop\/samples\/EchoBot-0\",\"skills\":{}}", + "path": "/Users/tester/Desktop/EmptyBot-1/EmptyBot-1.botproj", + "relativePath": "dialogs/additem/language-understanding/en-us/additem.en-us.lu", + "lastModified": "Thu Jul 09 2020 10:19:09 GMT-0700 (Pacific Daylight Time)" } ], "location": "/Users/tester/Desktop/EmptyBot-1", diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index 7387817b33..ded50ddb3c 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -7,6 +7,7 @@ import { useRecoilState } from 'recoil'; import httpClient from '../../../utils/httpUtil'; import { projectDispatcher } from '../project'; +import { botProjectFileDispatcher } from '../botProjectFile'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; import { recentProjectsState, @@ -29,7 +30,9 @@ import { locationState, skillsState, botStatusState, - botNameState, + botDisplayNameState, + botOpeningState, + botProjectFileState, } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '../../dispatchers'; @@ -76,7 +79,7 @@ describe('Project dispatcher', () => { const schemas = useRecoilValue(schemasState(projectId)); const location = useRecoilValue(locationState(projectId)); const skills = useRecoilValue(skillsState(projectId)); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); const lgFiles = useRecoilValue(lgFilesState(projectId)); @@ -95,6 +98,8 @@ describe('Project dispatcher', () => { const boilerplateVersion = useRecoilValue(boilerplateVersionState); const templates = useRecoilValue(templateProjectsState); const runtimeTemplates = useRecoilValue(runtimeTemplatesState); + const botOpening = useRecoilValue(botOpeningState); + const [botProjectFile, setBotProjectFile] = useRecoilState(botProjectFileState(projectId)); return { skillManifests, @@ -114,12 +119,15 @@ describe('Project dispatcher', () => { currentDispatcher, recentProjects, appError, - setRecentProjects, templateId, announcement, boilerplateVersion, templates, runtimeTemplates, + botOpening, + botProjectFile, + setBotProjectFile, + setRecentProjects, }; }; @@ -135,6 +143,7 @@ describe('Project dispatcher', () => { recoilState: dispatcherState, initialValue: { projectDispatcher, + botProjectFileDispatcher, }, }, } @@ -143,13 +152,23 @@ describe('Project dispatcher', () => { dispatcher = renderedComponent.current.currentDispatcher; }); - it('should open bot project', async () => { - let result; + it('should throw an error if no bot project file is present in the bot', async () => { (httpClient.put as jest.Mock).mockResolvedValueOnce({ data: mockProjectResponse, }); await act(async () => { - result = await dispatcher.openProject('../test/empty-bot', 'default'); + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + expect(navigateTo).toHaveBeenLastCalledWith(`/home`); + expect(renderedComponent.current.appError).toBeUndefined(); + }); + + fit('should open bot project', async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mockProjectResponse, + }); + await act(async () => { + await dispatcher.openProject('../test/empty-bot', 'default'); }); expect(renderedComponent.current.projectId).toBe(mockProjectResponse.id); @@ -159,12 +178,11 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.luFiles.length).toBe(1); expect(renderedComponent.current.botEnvironment).toBe(mockProjectResponse.botEnvironment); expect(renderedComponent.current.skills.length).toBe(0); - // expect(renderedComponent.current.botOpening).toBeFalsy(); + expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.schemas.sdk).toBeDefined(); expect(renderedComponent.current.schemas.default).toBeDefined(); expect(renderedComponent.current.schemas.diagnostics?.length).toBe(0); expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/`); - expect(result).toBe(renderedComponent.current.projectId); }); it('should handle project failure if project does not exist', async () => { @@ -234,7 +252,7 @@ describe('Project dispatcher', () => { }); await act(async () => { await dispatcher.openProject('../test/empty-bot', 'default'); - await dispatcher.deleteBotProject(projectId); + await dispatcher.deleteBot(projectId); }); expect(renderedComponent.current.botName).toEqual(''); @@ -243,7 +261,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.luFiles.length).toBe(0); expect(renderedComponent.current.botEnvironment).toBe('production'); expect(renderedComponent.current.skills.length).toBe(0); - // expect(renderedComponent.current.botOpening).toBeFalsy(); + expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.schemas.sdk).toBeUndefined(); expect(renderedComponent.current.schemas.default).toBeUndefined(); expect(renderedComponent.current.schemas.diagnostics?.length).toBeUndefined(); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index 623d6aa703..0b50e4c909 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -4,93 +4,95 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpace, BotProjectSpaceSkill } from '@bfc/shared'; +import { BotProjectSpaceSkill } from '@bfc/shared'; -import { botNameState, botProjectFileState, locationState, skillManifestsState } from '../atoms'; -import { convertPathToFileProtocol, trimFileProtocol } from '../../utils/fileUtil'; +import { botNameIdentifierState, botProjectFileState, locationState } from '../atoms'; +import { isBotProjectSpaceSelector, rootBotProjectIdSelector } from '../selectors'; +import { convertPathToFileProtocol } from '../../utils/fileUtil'; export const botProjectFileDispatcher = () => { - const addSkillToBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, skillId: string, remote: boolean) => { + const addLocalSkillToBotProjectFile = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const isBotProjectSpace = await snapshot.getPromise(isBotProjectSpaceSelector); + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!isBotProjectSpace || !rootBotProjectId) { + return; + } const skillLocation = await snapshot.getPromise(locationState(skillId)); - const botName = await snapshot.getPromise(botNameState(skillId)); - const manifests: { id: string; content: string; lastModified: string }[] = await snapshot.getPromise( - skillManifestsState(skillId) - ); - // TODO:// We would support only 1 manifest per skill. It will always be the first manifest. We would need UI in future to set the default manifest file - const currentManifest = manifests[0]; + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); - set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { - const result = produce(current, (draftState: BotProjectSpace) => { + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { const skill: BotProjectSpaceSkill = { workspace: convertPathToFileProtocol(skillLocation), - remote, - name: botName, + remote: false, }; - if (currentManifest) { - skill.manifest = currentManifest.id; - } - draftState.skills.push(skill); + draftState.content.skills[botName] = skill; }); return result; }); } ); - const removeLocalSkillFromBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, skillId: string) => { - const skillLocation = await snapshot.getPromise(locationState(skillId)); - snapshot.getPromise(skillManifestsState(skillId)); + const addRemoteSkillToBotProjectFile = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillId: string, manifestUrl: string, endpointName: string) => { + const isBotProjectSpace = await snapshot.getPromise(isBotProjectSpaceSelector); + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!isBotProjectSpace || !rootBotProjectId) { + return; + } + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); - set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { - const result = produce(current, (draftState: BotProjectSpace) => { - draftState.skills = draftState.skills.filter(({ workspace }) => { - if (workspace) { - return trimFileProtocol(workspace) !== skillLocation; - } - return true; - }); - }); - return result; - }); - } - ); + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + const skill: BotProjectSpaceSkill = { + manifest: manifestUrl, + remote: true, + endpointName, + }; - const removeRemoteSkillFromBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (rootBotProjectId: string, projectId: string) => { - const manifestLocation = await snapshot.getPromise(locationState(projectId)); - set(botProjectFileState(rootBotProjectId), (current: BotProjectSpace) => { - const result = produce(current, (draftState: BotProjectSpace) => { - draftState.skills = draftState.skills.filter(({ manifest, remote }) => { - if (remote) { - return manifestLocation !== manifest; - } - return true; - }); + draftState.content.skills[botName] = skill; }); return result; }); } ); - const renameRootBotFromBotProject = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { - const location = await snapshot.getPromise(locationState(projectId)); - const botname = await snapshot.getPromise(botNameState(projectId)); - set(botProjectFileState(projectId), (current: BotProjectSpace) => { - const result = produce(current, (draftState: BotProjectSpace) => { - draftState.workspace = convertPathToFileProtocol(location); - draftState.name = botname; + const removeSkillFromBotProjectFile = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const isBotProjectSpace = await snapshot.getPromise(isBotProjectSpaceSelector); + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!isBotProjectSpace || !rootBotProjectId) { + return; + } + + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + delete draftState.content.skills[botName]; }); return result; }); } ); + // const renameRootBotInBotProjectFile = useRecoilCallback( + // ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { + // const location = await snapshot.getPromise(locationState(projectId)); + // const botname = await snapshot.getPromise(botNameState(projectId)); + // set(botProjectFileState(projectId), (current: BotProjectSpace) => { + // const result = produce(current, (draftState: BotProjectSpace) => { + // draftState.workspace = convertPathToFileProtocol(location); + // draftState.name = botname; + // }); + // return result; + // }); + // } + // ); + return { - addSkillToBotProject, - removeLocalSkillFromBotProject, - removeRemoteSkillFromBotProject, - renameRootBotFromBotProject, + addLocalSkillToBotProjectFile, + removeSkillFromBotProjectFile, + addRemoteSkillToBotProjectFile, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/export.ts b/Composer/packages/client/src/recoilModel/dispatchers/export.ts index 3a00d0f35a..ed5eb0ed33 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/export.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/export.ts @@ -5,13 +5,13 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import httpClient from '../../utils/httpUtil'; -import { botNameState } from '../atoms'; +import { botDisplayNameState } from '../atoms'; import { logMessage } from './shared'; export const exportDispatcher = () => { const exportToZip = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { - const botName = await callbackHelpers.snapshot.getPromise(botNameState(projectId)); + const botName = await callbackHelpers.snapshot.getPromise(botDisplayNameState(projectId)); try { const response = await httpClient.get(`/projects/${projectId}/export/`, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([response.data])); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts index 395748390a..ad03d0afdf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts @@ -18,7 +18,7 @@ import { onAddLanguageDialogCompleteState, onDelLanguageDialogCompleteState, showDelLanguageModalState, - botNameState, + botDisplayNameState, } from './../atoms/botState'; const copyLanguageResources = (files: any[], fromLanguage: string, toLanguages: string[]): any[] => { @@ -58,7 +58,7 @@ const deleteLanguageResources = ( export const multilangDispatcher = () => { const setLocale = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async (locale: string, projectId: string) => { - const botName = await snapshot.getPromise(botNameState(projectId)); + const botName = await snapshot.getPromise(botDisplayNameState(projectId)); set(localeState(projectId), locale); languageStorage.setLocale(botName, locale); @@ -68,7 +68,7 @@ export const multilangDispatcher = () => { const addLanguages = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ languages, defaultLang, switchTo = false, projectId }) => { const { set, snapshot } = callbackHelpers; - const botName = await snapshot.getPromise(botNameState(projectId)); + const botName = await snapshot.getPromise(botDisplayNameState(projectId)); const prevlgFiles = await snapshot.getPromise(lgFilesState(projectId)); const prevluFiles = await snapshot.getPromise(luFilesState(projectId)); const prevSettings = await snapshot.getPromise(settingsState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 76d0fbc86b..65e469bbcb 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -4,7 +4,7 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import formatMessage from 'format-message'; -import isNumber from 'lodash/isNumber'; +import findIndex from 'lodash/findIndex'; import httpClient from '../../utils/httpUtil'; import { BotStatus } from '../../constants'; @@ -15,11 +15,15 @@ import { navigateTo } from '../../utils/navigation'; import { projectIdCache } from '../../utils/projectCache'; import { botProjectIdsState, - currentProjectIdState, botStatusState, botOpeningState, projectMetaDataState, + currentProjectIdState, + botErrorState, + botNameIdentifierState, } from '../atoms'; +import { dispatcherState } from '../DispatcherWrapper'; +import { getFileNameFromPath } from '../../utils/fileUtil'; import { recentProjectsState, templateIdState, announcementState, boilerplateVersionState } from './../atoms'; import { logMessage, setError } from './../dispatchers/shared'; @@ -28,22 +32,48 @@ import { handleProjectFailure, navigateToBot, openLocalSkill, - initBotState, + saveProject, removeRecentProject, createNewBotFromTemplate, resetBotStates, openRemoteSkill, openRootBotAndSkillsByProjectId, + checkIfBotExistsInBotProjectFile, + getSkillNameIdentifier, openRootBotAndSkillsByPath, } from './utils/project'; export const projectDispatcher = () => { const removeSkillFromBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string) => { + (callbackHelpers: CallbackInterface) => async (projectIdToRemove: string) => { try { - const { set } = callbackHelpers; - set(botProjectIdsState, (currentProjects) => currentProjects.filter((id) => id !== projectId)); - resetBotStates(callbackHelpers, projectId); + const { set, snapshot } = callbackHelpers; + const dispatcher = await snapshot.getPromise(dispatcherState); + await dispatcher.removeSkillFromBotProjectFile(projectIdToRemove); + + set(botProjectIdsState, (currentProjects) => { + const filtered = currentProjects.filter((id) => id !== projectIdToRemove); + return filtered; + }); + resetBotStates(callbackHelpers, projectIdToRemove); + } catch (ex) { + setError(callbackHelpers, ex); + } + } + ); + + const replaceSkillInBotProject = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (projectIdToRemove: string, path: string, storageId = 'default') => { + try { + const { snapshot } = callbackHelpers; + const dispatcher = await snapshot.getPromise(dispatcherState); + const projectIds = await snapshot.getPromise(botProjectIdsState); + const indexToReplace = findIndex(projectIds, (id) => id === projectIdToRemove); + if (indexToReplace === -1) { + return; + } + await dispatcher.removeSkillFromBotProject(projectIdToRemove); + await dispatcher.addExistingSkillToBotProject(path, storageId); } catch (ex) { setError(callbackHelpers, ex); } @@ -51,21 +81,29 @@ export const projectDispatcher = () => { ); const addExistingSkillToBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default', pushIndex: number) => { - const { set } = callbackHelpers; + (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default'): Promise => { + const { set, snapshot } = callbackHelpers; try { set(botOpeningState, true); - const { projectId } = await openLocalSkill(callbackHelpers, path, storageId); - if (isNumber(pushIndex)) { - set(botProjectIdsState, (current: string[]) => { - const mutated = [...current]; - mutated.splice(pushIndex, 0, projectId); - return mutated; - }); - } else { - set(botProjectIdsState, (current) => [...current, projectId]); + const dispatcher = await snapshot.getPromise(dispatcherState); + const botExists = await checkIfBotExistsInBotProjectFile(callbackHelpers, path); + if (botExists) { + throw new Error( + formatMessage('This operation cannot be completed. The skill is already part of the Bot Project') + ); } - return projectId; + const skillNameIdentifier: string = await getSkillNameIdentifier(callbackHelpers, getFileNameFromPath(path)); + + const { projectId, mainDialog } = await openLocalSkill(callbackHelpers, path, storageId, skillNameIdentifier); + if (!mainDialog) { + const error = await snapshot.getPromise(botErrorState(projectId)); + throw error; + } + + set(botProjectIdsState, (current) => [...current, projectId]); + await dispatcher.addLocalSkillToBotProjectFile(projectId); + } catch (ex) { + handleProjectFailure(callbackHelpers, ex); } finally { set(botOpeningState, false); } @@ -74,11 +112,22 @@ export const projectDispatcher = () => { const addRemoteSkillToBotProject = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { - const { set } = callbackHelpers; + const { set, snapshot } = callbackHelpers; try { + const dispatcher = await snapshot.getPromise(dispatcherState); + const botExists = await checkIfBotExistsInBotProjectFile(callbackHelpers, manifestUrl, true); + if (botExists) { + throw new Error( + formatMessage('This operation cannot be completed. The skill is already part of the Bot Project') + ); + } + const skillNameIdentifier: string = await getSkillNameIdentifier(callbackHelpers, name); set(botOpeningState, true); - const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl, name); - return projectId; + const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl, skillNameIdentifier); + set(botProjectIdsState, (current) => [...current, projectId]); + await dispatcher.addRemoteSkillToBotProjectFile(projectId, manifestUrl, endpointName); + } catch (ex) { + handleProjectFailure(callbackHelpers, ex); } finally { set(botOpeningState, false); } @@ -86,18 +135,13 @@ export const projectDispatcher = () => { ); const addNewSkillToBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async ( - templateId: string, - name: string, - description: string, - location: string, - schemaUrl?: string, - locale?: string, - qnaKbUrls?: string[] - ) => { - const { set } = callbackHelpers; + (callbackHelpers: CallbackInterface) => async (newProjectData: any) => { + const { set, snapshot } = callbackHelpers; + const dispatcher = await snapshot.getPromise(dispatcherState); try { + const { templateId, name, description, location, schemaUrl, locale, qnaKbUrls } = newProjectData; set(botOpeningState, true); + const { projectId, mainDialog } = await createNewBotFromTemplate( callbackHelpers, templateId, @@ -107,15 +151,17 @@ export const projectDispatcher = () => { schemaUrl, locale ); + const skillNameIdentifier: string = await getSkillNameIdentifier(callbackHelpers, getFileNameFromPath(name)); + set(botNameIdentifierState(projectId), skillNameIdentifier); set(projectMetaDataState(projectId), { isRemote: false, isRootBot: false, }); set(botProjectIdsState, (current) => [...current, projectId]); - navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); + await dispatcher.addLocalSkillToBotProjectFile(projectId); + navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls, templateId); return projectId; } catch (ex) { - set(botProjectIdsState, []); handleProjectFailure(callbackHelpers, ex); } finally { set(botOpeningState, false); @@ -127,17 +173,22 @@ export const projectDispatcher = () => { (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { const { set } = callbackHelpers; try { - await flushExistingTasks(callbackHelpers); set(botOpeningState, true); - set(botProjectIdsState, []); + await flushExistingTasks(callbackHelpers); const { projectId, mainDialog } = await openRootBotAndSkillsByPath(callbackHelpers, path, storageId); + + // Post project creation + set(projectMetaDataState(projectId), { + isRootBot: true, + isRemote: false, + }); projectIdCache.set(projectId); - navigateToBot(projectId, mainDialog); + navigateToBot(callbackHelpers, projectId, mainDialog); } catch (ex) { set(botProjectIdsState, []); - projectIdCache.clear(); removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); + navigateTo('/home'); } finally { set(botOpeningState, false); } @@ -149,11 +200,16 @@ export const projectDispatcher = () => { try { await flushExistingTasks(callbackHelpers); set(botOpeningState, true); - set(botProjectIdsState, []); await openRootBotAndSkillsByProjectId(callbackHelpers, projectId); + + // Post project creation + set(projectMetaDataState(projectId), { + isRootBot: true, + isRemote: false, + }); + projectIdCache.set(projectId); } catch (ex) { set(botProjectIdsState, []); - projectIdCache.clear(); handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); } finally { @@ -161,39 +217,61 @@ export const projectDispatcher = () => { } }); - const createProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async ( - templateId: string, - name: string, - description: string, - location: string, - schemaUrl?: string, - locale?: string, - qnaKbUrls?: string[] - ) => { + const createNewBot = useRecoilCallback((callbackHelpers: CallbackInterface) => async (newProjectData: any) => { + const { set } = callbackHelpers; + try { + await flushExistingTasks(callbackHelpers); + set(botOpeningState, true); + const { templateId, name, description, location, schemaUrl, locale, qnaKbUrls } = newProjectData; + const { projectId, mainDialog } = await createNewBotFromTemplate( + callbackHelpers, + templateId, + name, + description, + location, + schemaUrl, + locale + ); + set(botProjectIdsState, [projectId]); + + // Post project creation + set(projectMetaDataState(projectId), { + isRootBot: true, + isRemote: false, + }); + projectIdCache.set(projectId); + navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls); + } catch (ex) { + set(botProjectIdsState, []); + handleProjectFailure(callbackHelpers, ex); + navigateTo('/home'); + } finally { + set(botOpeningState, false); + } + }); + + const saveProjectAs = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (oldProjectId, name, description, location) => { const { set } = callbackHelpers; try { await flushExistingTasks(callbackHelpers); set(botOpeningState, true); - set(botProjectIdsState, []); - const { projectId, mainDialog } = await createNewBotFromTemplate( - callbackHelpers, - templateId, + const { projectId, mainDialog } = await saveProject(callbackHelpers, { + oldProjectId, name, description, location, - schemaUrl, - locale - ); + }); + + // Post project creation + set(projectMetaDataState(projectId), { + isRootBot: true, + isRemote: false, + }); projectIdCache.set(projectId); - set(botProjectIdsState, [projectId]); - set(currentProjectIdState, projectId); - navigateToBot(projectId, mainDialog, qnaKbUrls, templateId); - return projectId; + navigateToBot(callbackHelpers, projectId, mainDialog); } catch (ex) { set(botProjectIdsState, []); - projectIdCache.clear(); - removeRecentProject(callbackHelpers, location); handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); } finally { @@ -202,38 +280,22 @@ export const projectDispatcher = () => { } ); - const deleteBotProject = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { + const deleteBot = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { try { + const { reset } = callbackHelpers; await httpClient.delete(`/projects/${projectId}`); luFileStatusStorage.removeAllStatuses(projectId); qnaFileStatusStorage.removeAllStatuses(projectId); settingStorage.remove(projectId); projectIdCache.clear(); resetBotStates(callbackHelpers, projectId); + reset(botProjectIdsState); + reset(currentProjectIdState); } catch (e) { logMessage(callbackHelpers, e.message); } }); - const saveProjectAs = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId, name, description, location) => { - try { - await flushExistingTasks(callbackHelpers); - const response = await httpClient.post(`/projects/${projectId}/project/saveAs`, { - storageId: 'default', - name, - description, - location, - }); - await initBotState(callbackHelpers, response.data, true); - return response.data.id; - } catch (ex) { - handleProjectFailure(callbackHelpers, ex); - logMessage(callbackHelpers, ex.message); - } - } - ); - const fetchRecentProjects = useRecoilCallback((callbackHelpers: CallbackInterface) => async () => { const { set } = callbackHelpers; try { @@ -278,8 +340,8 @@ export const projectDispatcher = () => { return { openProject, - createProject, - deleteBotProject, + createNewBot, + deleteBot, saveProjectAs, fetchProjectById, fetchRecentProjects, @@ -291,5 +353,6 @@ export const projectDispatcher = () => { addNewSkillToBotProject, addExistingSkillToBotProject, addRemoteSkillToBotProject, + replaceSkillInBotProject, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/shared.ts b/Composer/packages/client/src/recoilModel/dispatchers/shared.ts index 811de1fd41..a930d50b10 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/shared.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/shared.ts @@ -41,12 +41,18 @@ export const setError = (callbackHelpers: CallbackInterface, payload) => { ), summary: formatMessage('Modification Rejected'), }); + } else if (payload?.response?.data?.message) { + callbackHelpers.set(applicationErrorState, payload.response.data); + } else if (payload instanceof Error) { + callbackHelpers.set(applicationErrorState, { + summary: payload.name, + message: payload.message, + }); } else { - if (payload?.response?.data?.message) { - callbackHelpers.set(applicationErrorState, payload.response.data); - } else { - callbackHelpers.set(applicationErrorState, payload); - } + callbackHelpers.set(applicationErrorState, payload); + } + if (payload != null) { + const message = JSON.stringify(payload); + logMessage(callbackHelpers, `Error: ${message}`); } - if (payload != null) logMessage(callbackHelpers, `Error: ${JSON.stringify(payload)}`); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 4e74f274ab..e77c29f195 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import path from 'path'; import { v4 as uuid } from 'uuid'; import { @@ -10,14 +9,20 @@ import { dereferenceDefinitions, DialogInfo, LuFile, - BotProjectSpace, QnAFile, + BotProjectSpace, + BotProjectFile, + SkillSetting, + BotProjectSpaceSkill, + getEndpointNameGivenUrl, } from '@bfc/shared'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; import { indexer, validateDialog } from '@bfc/indexers'; import { CallbackInterface } from 'recoil'; import { stringify } from 'query-string'; +import formatMessage from 'format-message'; +import camelCase from 'lodash/camelCase'; import * as botstates from '../../atoms/botState'; import UndoHistory from '../../undo/undoHistory'; @@ -33,8 +38,8 @@ import { projectMetaDataState, qnaFilesState, recentProjectsState, - botOpeningState, botErrorState, + botNameIdentifierState, } from '../../atoms'; import lgWorker from '../../parsers/lgWorker'; import luWorker from '../../parsers/luWorker'; @@ -58,22 +63,22 @@ import { lgFilesState, locationState, botStatusState, - botNameState, + botDisplayNameState, botEnvironmentState, dialogsState, dialogSchemasState, } from '../../atoms'; import { undoHistoryState } from '../../undo/history'; +import { rootBotProjectIdSelector } from '../../selectors'; +import { trimFileProtocol, getUniqueName } from '../../../utils/fileUtil'; +import { dispatcherState } from '../../DispatcherWrapper'; -export const resetBotStates = async ({ snapshot, gotoSnapshot }: CallbackInterface, projectId: string) => { +export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); - const newSnapshot = snapshot.map(({ reset }) => { - botStates.forEach((state) => { - const currentRecoilAtom: any = botstates[state]; - reset(currentRecoilAtom(projectId)); - }); + botStates.forEach((state) => { + const currentRecoilAtom: any = botstates[state]; + reset(currentRecoilAtom(projectId)); }); - gotoSnapshot(newSnapshot); }; export const setErrorOnBotProject = async ( @@ -93,16 +98,15 @@ export const setErrorOnBotProject = async ( export const flushExistingTasks = async (callbackHelpers) => { const { snapshot, reset } = callbackHelpers; + reset(botProjectSpaceLoadedState); const projectIds = await snapshot.getPromise(botProjectIdsState); - const recoilTasks: any[] = []; + reset(botProjectIdsState, []); for (const projectId of projectIds) { - const resetStates = await resetBotStates(callbackHelpers, projectId); - recoilTasks.push(resetStates); + resetBotStates(callbackHelpers, projectId); } - reset(botProjectIdsState); const workers = [lgWorker, luWorker, qnaWorker]; - return Promise.all([workers.map((w) => w.flush()), ...recoilTasks]); + return Promise.all([workers.map((w) => w.flush())]); }; // merge sensitive values in localStorage @@ -131,8 +135,16 @@ export const getMergedSettings = (projectId, settings): DialogSetting => { return mergedSettings; }; -export const navigateToBot = (projectId: string, mainDialog: string, qnaKbUrls?: string[], templateId?: string) => { +export const navigateToBot = ( + callbackHelpers: CallbackInterface, + projectId: string, + mainDialog: string, + qnaKbUrls?: string[], + templateId?: string +) => { if (projectId) { + const { set } = callbackHelpers; + set(currentProjectIdState, projectId); let url = `/bot/${projectId}/dialogs/${mainDialog}`; if (templateId === QnABotTemplateId) { url = `/bot/${projectId}/knowledge-base/${mainDialog}`; @@ -157,7 +169,7 @@ const loadProjectData = (response) => { }; export const fetchProjectDataByPath = async ( - path, + path: string, storageId ): Promise<{ botFiles: any; projectData: any; error: any }> => { try { @@ -176,7 +188,7 @@ export const fetchProjectDataByPath = async ( export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { try { const response = await httpClient.get(`/projects/${projectId}`); - const { botFiles, projectData } = loadProjectData(response); + const projectData = loadProjectData(response); return projectData; } catch (ex) { return { @@ -187,21 +199,7 @@ export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; } }; -export const parseFileProtocolPaths = (rootPath: string, relativePath): string => { - try { - const rootPathWithoutProtocol = rootPath.replace('file://', ''); - let skillPath = relativePath.replace('file://', ''); - if (skillPath) { - skillPath = path.resolve(rootPathWithoutProtocol, skillPath); - } - return path.normalize(skillPath); - } catch (ex) { - throw new Error('Invalid path'); - } -}; - export const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { - callbackHelpers.set(botProjectSpaceLoadedState, false); setError(callbackHelpers, ex); }; @@ -291,7 +289,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(dialogsState(projectId), verifiedDialogs); set(dialogSchemasState(projectId), dialogSchemas); set(botEnvironmentState(projectId), botEnvironment); - set(botNameState(projectId), botName); + set(botDisplayNameState(projectId), botName); set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs)); if (location !== curLocation) { set(botStatusState(projectId), BotStatus.unConnected); @@ -323,51 +321,47 @@ export const removeRecentProject = async (callbackHelpers: CallbackInterface, pa } }; -export const openRemoteSkill = async (callbackHelpers: CallbackInterface, manifestUrl: string, name: string) => { +export const openRemoteSkill = async ( + callbackHelpers: CallbackInterface, + manifestUrl: string, + botNameIdentifier: string +) => { const { set } = callbackHelpers; - try { - const response = await httpClient.get(`/projects/generate-projectId`); - const projectId = response.data; - const stringified = stringify({ - url: manifestUrl, - }); - const manifestResponse = await httpClient.get( - `/projects/${projectId}/skill/retrieve-skill-manifest?${stringified}` - ); - set(projectMetaDataState(projectId), { - isRootBot: false, - isRemote: true, - }); - set(botNameState(projectId), manifestResponse.data.name); - set(locationState(projectId), manifestUrl); - return { projectId }; - } catch (ex) { - const tempProjectId = uuid(); - set(botNameState(tempProjectId), name); - setErrorOnBotProject(callbackHelpers, tempProjectId, name, ex); - return { projectId: tempProjectId }; - } + + const response = await httpClient.get(`/projects/generate-projectId`); + const projectId = response.data; + const stringified = stringify({ + url: manifestUrl, + }); + const manifestResponse = await httpClient.get(`/projects/${projectId}/skill/retrieve-skill-manifest?${stringified}`); + set(projectMetaDataState(projectId), { + isRootBot: false, + isRemote: true, + }); + set(botNameIdentifierState(projectId), botNameIdentifier); + set(botDisplayNameState(projectId), manifestResponse.data.name); + set(locationState(projectId), manifestUrl); + return { projectId, manifestResponse: manifestResponse.data }; }; -export const openLocalSkill = async (callbackHelpers, pathToBot: string, storageId, name?: string) => { +export const openLocalSkill = async (callbackHelpers, pathToBot: string, storageId, botNameIdentifier: string) => { const { set } = callbackHelpers; const { projectData, botFiles, error } = await fetchProjectDataByPath(pathToBot, storageId); - if (!error) { - const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); - set(projectMetaDataState(projectData.id), { - isRootBot: false, - isRemote: false, - }); - return { - projectId: projectData.id, - mainDialog, - }; - } else { - const tempProjectId = uuid(); - set(botNameState(tempProjectId), name); - setErrorOnBotProject(callbackHelpers, tempProjectId, name, error); - return tempProjectId; + + if (error) { + throw error; } + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); + set(projectMetaDataState(projectData.id), { + isRootBot: false, + isRemote: false, + }); + set(botNameIdentifierState(projectData.id), botNameIdentifier); + + return { + projectId: projectData.id, + mainDialog, + }; }; export const createNewBotFromTemplate = async ( @@ -379,6 +373,7 @@ export const createNewBotFromTemplate = async ( schemaUrl?: string, locale?: string ) => { + const { set } = callbackHelpers; const response = await httpClient.post(`/projects`, { storageId: 'default', templateId, @@ -393,54 +388,118 @@ export const createNewBotFromTemplate = async ( if (settingStorage.get(projectId)) { settingStorage.remove(projectId); } + const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; + set(botProjectFileState(projectId), currentBotProjectFileIndexed); const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); return { projectId, mainDialog }; }; const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => { + let isBotProjectLoaded = false; set(botProjectIdsState, (current: string[]) => { const botProjectIDs = [...current, projectId]; if (botProjectIDs.length === skillCt) { - set(botProjectSpaceLoadedState, true); + isBotProjectLoaded = true; } return botProjectIDs; }); + if (isBotProjectLoaded) { + set(botProjectSpaceLoadedState, true); + } +}; + +const handleSkillLoadingFailure = (callbackHelpers, { ex, skillNameIdentifier }) => { + const { set } = callbackHelpers; + // Generating a dummy project id which will be replaced by the user from the UI. + const projectId = uuid(); + set(botDisplayNameState(projectId), skillNameIdentifier); + set(botNameIdentifierState(projectId), skillNameIdentifier); + setErrorOnBotProject(callbackHelpers, projectId, skillNameIdentifier, ex); + return { projectId: projectId }; +}; + +const mapToRemoteSkill = async (skillsSettings: { + [skillName: string]: SkillSetting; +}): Promise<{ mappedSkills: { [name: string]: BotProjectSpaceSkill }; endpointsMap: Map }> => { + const endpointsMap = new Map(); + try { + const mappedSkills: { [name: string]: BotProjectSpaceSkill } = {}; + for (const skillName in skillsSettings) { + const val = skillsSettings[skillName]; + mappedSkills[skillName] = { + manifest: val.manifestUrl || '', + remote: true, + }; + endpointsMap.set(skillName, val.endpointUrl || ''); + } + return { mappedSkills, endpointsMap }; + } catch (ex) { + return { mappedSkills: {}, endpointsMap: endpointsMap }; + } }; const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { const { projectData, botFiles } = data; - const { set } = callbackHelpers; + const { set, snapshot } = callbackHelpers; + const dispatcher = await snapshot.getPromise(dispatcherState); - const mainDialog = await initBotState(callbackHelpers, projectData, botFiles, true); + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); const rootBotProjectId = projectData.id; + const { name, settings } = projectData; + set(botNameIdentifierState(rootBotProjectId), camelCase(name)); + + if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { + const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; + set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); + const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; + + const { mappedSkills, endpointsMap } = await mapToRemoteSkill(settings?.skill); + const skillsToAddToBotProjectFile = Object.keys(mappedSkills); + const skills: { [skillId: string]: BotProjectSpaceSkill } = { + ...currentBotProjectFile.skills, + ...mappedSkills, + }; - if (botFiles.botProjectSpaceFiles.length) { - const currentBotProjectFile: BotProjectSpace = botFiles.botProjectSpaceFiles[0]; - set(botProjectFileState(rootBotProjectId), currentBotProjectFile); - - const skillsInBotProject = currentBotProjectFile.skills; - if (skillsInBotProject.length) { - for (const skillInBotProject of skillsInBotProject) { - if (!skillInBotProject.remote) { - const path = parseFileProtocolPaths(currentBotProjectFile.workspace, skillInBotProject.workspace); - //TODO handle exception - openLocalSkill(callbackHelpers, path, storageId, skillInBotProject.name).then((result) => - addProjectToBotProjectSpace(set, result.projectId, skillsInBotProject.length) - ); - } else { - if (skillInBotProject.manifest) { - openRemoteSkill(callbackHelpers, skillInBotProject.manifest, skillInBotProject.name).then((result) => - addProjectToBotProjectSpace(set, result.projectId, skillsInBotProject.length) - ); - } + // RootBot loads first + skills load async + const totalProjectsCount = Object.keys(skills).length + 1; + if (totalProjectsCount > 0) { + for (const nameIdentifier in skills) { + const skill = skills[nameIdentifier]; + let skillPromise; + if (!skill.remote && skill.workspace) { + const skillPath = trimFileProtocol(skill.workspace); + skillPromise = openLocalSkill(callbackHelpers, skillPath, storageId, nameIdentifier); + } else if (skill.manifest) { + skillPromise = openRemoteSkill(callbackHelpers, skill.manifest, nameIdentifier); + } + if (skillPromise) { + skillPromise + .then(({ manifestResponse, projectId }) => { + addProjectToBotProjectSpace(set, projectId, totalProjectsCount); + if (skillsToAddToBotProjectFile.includes(nameIdentifier) && skill.manifest) { + const endpointUrl = endpointsMap.get(nameIdentifier) || ''; + const endpointName = getEndpointNameGivenUrl(manifestResponse, endpointUrl); + dispatcher.addRemoteSkillToBotProjectFile(projectId, skill.manifest, endpointName); + } + }) + .catch((ex) => { + const { projectId } = handleSkillLoadingFailure(callbackHelpers, { + skillNameIdentifier: nameIdentifier, + ex, + }); + addProjectToBotProjectSpace(set, projectId, totalProjectsCount); + if (skillsToAddToBotProjectFile.includes(nameIdentifier) && skill.manifest) { + dispatcher.addRemoteSkillToBotProjectFile(projectId, skill.manifest, ''); + } + }); } } } } else { - set(botProjectSpaceLoadedState, true); + // Should never hit here as all projects should have a botproject file + throw new Error(formatMessage('Bot project file does not exist.')); } set(botProjectIdsState, [rootBotProjectId]); - set(botOpeningState, false); set(currentProjectIdState, rootBotProjectId); return { mainDialog, @@ -450,12 +509,75 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st export const openRootBotAndSkillsByPath = async (callbackHelpers: CallbackInterface, path: string, storageId) => { const data = await fetchProjectDataByPath(path, storageId); - const result = await openRootBotAndSkills(callbackHelpers, data, storageId); - return result; + if (data.error) { + throw data.error; + } + return await openRootBotAndSkills(callbackHelpers, data, storageId); }; export const openRootBotAndSkillsByProjectId = async (callbackHelpers: CallbackInterface, projectId: string) => { const data = await fetchProjectDataById(projectId); - const result = await openRootBotAndSkills(callbackHelpers, data); + if (data.error) { + throw data.error; + } + return await openRootBotAndSkills(callbackHelpers, data); +}; + +export const saveProject = async (callbackHelpers, oldProjectData) => { + const { oldProjectId, name, description, location } = oldProjectData; + const response = await httpClient.post(`/projects/${oldProjectId}/project/saveAs`, { + storageId: 'default', + name, + description, + location, + }); + const data = loadProjectData(response); + if (data.error) { + throw data.error; + } + const result = openRootBotAndSkills(callbackHelpers, data); return result; }; + +export const getSkillNameIdentifier = async ( + callbackHelpers: CallbackInterface, + displayName: string +): Promise => { + const { snapshot } = callbackHelpers; + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (rootBotProjectId) { + const { content: botProjectFile } = await snapshot.getPromise(botProjectFileState(rootBotProjectId)); + return getUniqueName(Object.keys(botProjectFile.skills), camelCase(displayName)); + } + return ''; +}; + +export const checkIfBotExistsInBotProjectFile = async ( + callbackHelpers: CallbackInterface, + pathOrManifest: string, + remote?: boolean +) => { + const { snapshot } = callbackHelpers; + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + throw new Error(formatMessage('The root bot is not a bot project')); + } + const { content: botProjectFile } = await snapshot.getPromise(botProjectFileState(rootBotProjectId)); + + for (const uniqueSkillName in botProjectFile.skills) { + const { manifest, workspace } = botProjectFile.skills[uniqueSkillName]; + if (remote) { + if (manifest === pathOrManifest) { + return true; + } + } else { + if (workspace) { + const resolvedPath = trimFileProtocol(workspace); + if (pathOrManifest === resolvedPath) { + return true; + } + } + } + } + return false; +}; diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts index 16a098ea40..8de0fec231 100644 --- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts +++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts @@ -3,7 +3,7 @@ import keys from 'lodash/keys'; import differenceWith from 'lodash/differenceWith'; import isEqual from 'lodash/isEqual'; -import { DialogInfo, DialogSchemaFile, DialogSetting, SkillManifest, BotAssets } from '@bfc/shared'; +import { DialogInfo, DialogSchemaFile, DialogSetting, SkillManifest, BotAssets, BotProjectFile } from '@bfc/shared'; import { LuFile, LgFile, QnAFile } from './../../../../lib/shared/src/types/indexers'; import * as client from './http'; @@ -184,6 +184,20 @@ class FilePersistence { return changes; } + private getBotProjectFileChanges(current: BotProjectFile, previous: BotProjectFile) { + if (!isEqual(current, previous)) { + return [ + { + id: `${current.id}${FileExtensions.BotProject}`, + change: JSON.stringify(current.content, null, 2), + type: ChangeType.UPDATE, + projectId: this._projectId, + }, + ]; + } + return []; + } + private getSettingsChanges(current: DialogSetting, previous: DialogSetting) { if (!isEqual(current, previous)) { return [ @@ -209,6 +223,12 @@ class FilePersistence { previousAssets.skillManifests ); const settingChanges = this.getSettingsChanges(currentAssets.setting, previousAssets.setting); + + const botProjectFileChanges = this.getBotProjectFileChanges( + currentAssets.botProjectFile, + previousAssets.botProjectFile + ); + const fileChanges: IFileChange[] = [ ...dialogChanges, ...dialogSchemaChanges, @@ -217,6 +237,7 @@ class FilePersistence { ...lgChanges, ...skillManifestChanges, ...settingChanges, + ...botProjectFileChanges, ]; return fileChanges; } diff --git a/Composer/packages/client/src/recoilModel/persistence/types.ts b/Composer/packages/client/src/recoilModel/persistence/types.ts index d1e344c4d0..f31373a543 100644 --- a/Composer/packages/client/src/recoilModel/persistence/types.ts +++ b/Composer/packages/client/src/recoilModel/persistence/types.ts @@ -15,6 +15,7 @@ export enum FileExtensions { Lg = '.lg', QnA = '.qna', Setting = 'appsettings.json', + BotProject = '.botproj', } export type FileErrorHandler = (error) => void; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index a5869ff077..9f976b162f 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -2,95 +2,28 @@ // Licensed under the MIT License. import { selector } from 'recoil'; +import isEmpty from 'lodash/isEmpty'; -import { dispatcherState } from '../DispatcherWrapper'; -import { Dispatcher } from '../dispatchers'; -import { botErrorState, botNameState, botProjectIdsState, dialogsState, projectMetaDataState } from '../atoms'; +import { + botErrorState, + botDisplayNameState, + botProjectFileState, + botProjectIdsState, + dialogsState, + projectMetaDataState, + botNameIdentifierState, +} from '../atoms'; // Actions -const projectLoadAction = (dispatcher: Dispatcher) => { - return { - createProject: async ( - templateId: string, - name: string, - description: string, - location: string, - schemaUrl?: string, - locale?: string, - qnaKbUrls?: string[] - ) => { - await dispatcher.createProject(templateId, name, description, location, schemaUrl, locale, qnaKbUrls); - }, - openProject: async (path: string, storageId = 'default') => { - await dispatcher.openProject(path, storageId); - }, - saveProjectAs: async (projectId: string, name: string, description: string, location: string) => { - await dispatcher.saveProjectAs(projectId, name, description, location); - }, - fetchProjectById: async (projectId: string) => { - await dispatcher.fetchProjectById(projectId); - }, - addExistingSkillToBotProject: async (rootBotId: string, path: string, storageId = 'default') => { - const skillId: string = await dispatcher.addExistingSkillToBotProject(path, storageId); - await dispatcher.addSkillToBotProject(rootBotId, skillId, false); - }, - addNewSkillToBotProject: async ( - rootBotId: string, - templateId: string, - name: string, - description: string, - location: string, - schemaUrl?: string, - locale?: string, - qnaKbUrls?: string[] - ) => { - const skillId: string = await dispatcher.addNewSkillToBotProject( - templateId, - name, - description, - location, - schemaUrl, - locale, - qnaKbUrls - ); - await dispatcher.addSkillToBotProject(rootBotId, skillId, false); - }, - addRemoteSkillToBotProject: async (rootBotId: string, manifestUrl: string, name: string, endpointName: string) => { - const skillId = await dispatcher.addRemoteSkillToBotProject(manifestUrl, name, endpointName); - await dispatcher.addSkillToBotProject(rootBotId, skillId, true); - }, - replaceSkillAtIndex: async (projectId: string, index: number, newPath: string, storageId = 'default') => { - await dispatcher.removeSkillFromBotProject(projectId); - await dispatcher.addExistingSkillToBotProject(newPath, storageId, index); - }, - }; -}; - -export const projectLoadSelector = selector({ - key: 'projectLoadSelector', - get: ({ get }) => { - const dispatcher = get(dispatcherState); - if (!dispatcher) { - return {} as Dispatcher; - } - return projectLoadAction(dispatcher); - }, -}); - -export const botProjectsWithoutErrorsSelector = selector({ - key: 'botProjectsWithoutErrorsSelector', +export const botsForFilePersistenceSelector = selector({ + key: 'botsForFilePersistenceSelector', get: ({ get }) => { const botProjectIds = get(botProjectIdsState); - const projectsWithoutErrors = botProjectIds - .filter((projectId) => !get(botErrorState(projectId))) - .map((projectId: string) => { - const metaData = get(projectMetaDataState(projectId)); - return { - projectId, - ...metaData, - }; - }); - return projectsWithoutErrors; + return botProjectIds.filter((projectId: string) => { + const { isRemote } = get(projectMetaDataState(projectId)); + const botError = get(botErrorState(projectId)); + return !botError && !isRemote; + }); }, }); @@ -102,9 +35,35 @@ export const botProjectSpaceSelector = selector({ const dialogs = get(dialogsState(projectId)); const metaData = get(projectMetaDataState(projectId)); const botError = get(botErrorState(projectId)); - const name = get(botNameState(projectId)); - return { dialogs, projectId, name, ...metaData, error: botError }; + const name = get(botDisplayNameState(projectId)); + const botNameId = get(botNameIdentifierState(projectId)); + return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); + console.log(result); return result; }, }); + +export const isBotProjectSpaceSelector = selector({ + key: 'isBotProjectSpaceSelector', + get: ({ get }) => { + const projectIds = get(botProjectIdsState); + const rootBotId = projectIds[0]; + const metaData = get(projectMetaDataState(rootBotId)); + const botProjectFile = get(botProjectFileState(rootBotId)); + return metaData.isRootBot && !isEmpty(botProjectFile); + }, +}); + +export const rootBotProjectIdSelector = selector({ + key: 'rootBotProjectIdSelector', + get: ({ get }) => { + const projectIds = get(botProjectIdsState); + const rootBotId = projectIds[0]; + + const metaData = get(projectMetaDataState(rootBotId)); + if (metaData.isRootBot) { + return rootBotId; + } + }, +}); diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index 7ed944c003..1df059fb53 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -12,13 +12,7 @@ import { resolveToBasePath } from './utils/fileUtil'; import { data } from './styles'; import { NotFound } from './components/NotFound'; import { BASEPATH } from './constants'; -import { - dispatcherState, - schemasState, - botProjectIdsState, - botProjectSpaceLoadedState, - botOpeningState, -} from './recoilModel'; +import { dispatcherState, schemasState, botProjectIdsState, botOpeningState } from './recoilModel'; import { openAlertModal } from './components/Modal/AlertDialog'; import { dialogStyle } from './components/Modal/dialogStyle'; import { LoadingSpinner } from './components/LoadingSpinner'; @@ -96,7 +90,6 @@ const ProjectRouter: React.FC> = (pro const schemas = useRecoilValue(schemasState(projectId)); const { fetchProjectById } = useRecoilValue(dispatcherState); const botProjects = useRecoilValue(botProjectIdsState); - const botsLoaded = useRecoilValue(botProjectSpaceLoadedState); useEffect(() => { if (props.projectId && !botProjects.includes(props.projectId)) { @@ -113,7 +106,7 @@ const ProjectRouter: React.FC> = (pro } }, [schemas, projectId]); - if (props.projectId && botsLoaded && botProjects.includes(props.projectId)) { + if (props.projectId && botProjects.includes(props.projectId)) { return
{props.children}
; } return ; diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index cbb8b68da1..bac740a100 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -22,7 +22,7 @@ import { localeState, qnaFilesState, designPageLocationState, - botNameState, + botDisplayNameState, dialogSchemasState, lgFilesState, luFilesState, @@ -54,7 +54,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const luFiles = useRecoilValue(luFilesState(projectId)); const lgFiles = useRecoilValue(lgFilesState(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); - const botName = useRecoilValue(botNameState(projectId)); + const botName = useRecoilValue(botDisplayNameState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const userSettings = useRecoilValue(userSettingsState); diff --git a/Composer/packages/client/src/utils/fileUtil.ts b/Composer/packages/client/src/utils/fileUtil.ts index 1f324737eb..d2b1360c4f 100644 --- a/Composer/packages/client/src/utils/fileUtil.ts +++ b/Composer/packages/client/src/utils/fileUtil.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import moment from 'moment'; import formatMessage from 'format-message'; import generate from 'format-message-generate-id'; @@ -105,9 +107,32 @@ export async function loadLocale(locale: string) { } export const trimFileProtocol = (path: string) => { + if (!path) { + return ''; + } return path.replace('file://', ''); }; export const convertPathToFileProtocol = (path: string) => { + if (!path) { + return ''; + } return `file://${path}`; }; + +export const getUniqueName = (list: string[], currentName: string) => { + let uniqueName = currentName; + const i = 1; + while (list.includes(currentName)) { + uniqueName += `-${i}`; + } + return uniqueName; +}; + +export const getFileNameFromPath = (param: string, ext: string | undefined = undefined) => { + return path.basename(param, ext).replace(/\\/g, '/'); +}; + +export const getAbsolutePath = (basePath: string, relativePath: string) => { + return path.resolve(basePath, relativePath); +}; diff --git a/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts b/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts index a47050c696..e2661d1c85 100644 --- a/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts +++ b/Composer/packages/lib/indexers/src/botProjectSpaceIndexer.ts @@ -3,12 +3,14 @@ import { BotProjectSpace, FileInfo } from '@bfc/shared'; +import { getBaseName } from './utils/help'; + const index = (botProjectSpaceFiles: FileInfo[]) => { // Handle botproject files for multiple env when Composer brings in Env return botProjectSpaceFiles.map((file) => { - const { content } = file; + const { content, lastModified, name } = file; const jsonContent: BotProjectSpace = JSON.parse(content); - return jsonContent; + return { content: jsonContent, id: getBaseName(name, '.botproj'), lastModified }; }); }; diff --git a/Composer/packages/lib/shared/src/skillsUtils/index.ts b/Composer/packages/lib/shared/src/skillsUtils/index.ts index dd977a19e2..03a4b4eb77 100644 --- a/Composer/packages/lib/shared/src/skillsUtils/index.ts +++ b/Composer/packages/lib/shared/src/skillsUtils/index.ts @@ -36,3 +36,8 @@ export const getSkillNameFromSetting = (value?: string) => { } return ''; }; + +export const getEndpointNameGivenUrl = (manifestData: any, urlToMatch: string) => { + const matchedEndpoint = manifestData?.endpoints.find(({ endpointUrl }) => endpointUrl === urlToMatch); + return matchedEndpoint ? matchedEndpoint.name : ''; +}; diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index c4dfe83591..00f0ef2f21 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -15,6 +15,7 @@ export enum FileExtensions { Qna = '.qna', Setting = 'appsettings.json', FormDialogSchema = '.form-dialog', + BotProject = '.botproj', } export interface FileInfo { @@ -182,6 +183,7 @@ export type BotAssets = { skillManifests: SkillManifest[]; setting: DialogSetting; dialogSchemas: DialogSchemaFile[]; + botProjectFile: BotProjectFile; }; export interface BotInfo { @@ -195,11 +197,18 @@ export interface BotProjectSpaceSkill { manifest?: string; remote: boolean; endpointName?: string; - name: string; } export interface BotProjectSpace { workspace: string; name: string; - skills: BotProjectSpaceSkill[]; + skills: { + [skillId: string]: BotProjectSpaceSkill; + }; +} + +export interface BotProjectFile { + id: string; + content: BotProjectSpace; + lastModified: string; } diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 1bf5b863c5..354637c71b 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -122,13 +122,6 @@ async function removeProject(req: Request, res: Response) { } async function openProject(req: Request, res: Response) { - if (!req.body.storageId || !req.body.path) { - res.status(400).json({ - message: 'parameters not provided, require stoarge id and path', - }); - return; - } - const user = await ExtensionContext.getUserFromRequest(req); const location: LocationRef = { diff --git a/Composer/packages/server/src/models/asset/assetManager.ts b/Composer/packages/server/src/models/asset/assetManager.ts index 0df9b9ca04..76a614fb20 100644 --- a/Composer/packages/server/src/models/asset/assetManager.ts +++ b/Composer/packages/server/src/models/asset/assetManager.ts @@ -18,11 +18,19 @@ import { BotProject } from '../bot/botProject'; export class AssetManager { public templateStorage: LocalDiskStorage; + private _botProjectFileTemplate; constructor() { this.templateStorage = new LocalDiskStorage(); } + public get botProjectFileTemplate() { + if (!this._botProjectFileTemplate) { + this._botProjectFileTemplate = this.getDefaultBotProjectTemplate(); + } + return this._botProjectFileTemplate; + } + public async getProjectTemplates() { return ExtensionContext.extensions.botTemplates; } @@ -116,4 +124,22 @@ export class AssetManager { return undefined; } } + + private getDefaultBotProjectTemplate() { + if (!ExtensionContext.extensions.botTemplates.length) { + return undefined; + } + const boilerplate = ExtensionContext.extensions.botTemplates[0]; + + const location = Path.join(boilerplate.path, `${boilerplate.id}.botproj`); + try { + if (fs.existsSync(location)) { + const raw = fs.readFileSync(location, 'utf8'); + const json = JSON.parse(raw); + return json; + } + } catch (err) { + return ''; + } + } } diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 5c8c8367fa..6accb5d219 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -19,6 +19,7 @@ import { ISettingManager, OBFUSCATED_VALUE } from '../settings'; import { DefaultSettingManager } from '../settings/defaultSettingManager'; import log from '../../logger'; import { BotProjectService } from '../../services/project'; +import AssetService from '../../services/asset'; import { Builder } from './builder'; import { IFileStorage } from './../storage/interface'; @@ -83,6 +84,17 @@ export class BotProject implements IBotProject { return files; } + public get botProjectFiles() { + const files: FileInfo[] = []; + this.files.forEach((file) => { + if (file.name.endsWith(FileExtensions.BotProject)) { + files.push(file); + } + }); + + return files; + } + public get dialogSchemaFiles() { const files: FileInfo[] = []; this.files.forEach((file) => { @@ -336,6 +348,14 @@ export class BotProject implements IBotProject { content.id = name; const updatedContent = autofixReferInDialog(botName, JSON.stringify(content, null, 2)); await this._updateFile(relativePath, updatedContent); + + for (const botProjectFile of this.botProjectFiles) { + const { relativePath } = botProjectFile; + const content = JSON.parse(botProjectFile.content); + content.workspace = this.dataDir; + content.name = botName; + await this._updateFile(relativePath, JSON.stringify(content, null, 2)); + } await serializeFiles(this.fileStorage, this.dataDir, botName); }; @@ -554,6 +574,7 @@ export class BotProject implements IBotProject { await pluginMethod.call(null, projectId); } } + if (ExtensionContext.extensions.publish[method]?.methods?.removeRuntimeData) { const pluginMethod = ExtensionContext.extensions.publish[method].methods.removeRuntimeData; if (typeof pluginMethod === 'function') { @@ -709,9 +730,16 @@ export class BotProject implements IBotProject { fileList.set(file.name, file); }); - const migrationFiles = await this._createQnAFilesForOldBot(fileList); + const migrationFilesList = await Promise.all([ + this._createQnAFilesForOldBot(fileList), + this._createBotProjectFileForOldBots(fileList), + ]); - return new Map([...fileList, ...migrationFiles]); + const files = [...fileList]; + migrationFilesList.forEach((migrationFiles) => { + files.push(...migrationFiles); + }); + return new Map(files); }; // migration: create qna files for old bots @@ -752,6 +780,35 @@ export class BotProject implements IBotProject { return fileList; }; + private _createBotProjectFileForOldBots = async (files: Map) => { + const fileList = new Map(); + try { + const defaultBotProjectFile: any = await AssetService.manager.botProjectFileTemplate; + + for (const [_, file] of files) { + if (file.name.endsWith(FileExtensions.BotProject)) { + return fileList; + } + } + const fileName = `${this.name}${FileExtensions.BotProject}`; + const root = this.dataDir; + + defaultBotProjectFile.workspace = root; + defaultBotProjectFile.name = this.name; + + await this._createFile(fileName, JSON.stringify(defaultBotProjectFile, null, 2)); + const pathToBotProject: string = Path.join(root, fileName); + const fileInfo = await this._getFileInfo(pathToBotProject); + + if (fileInfo) { + fileList.set(fileInfo.name, fileInfo); + } + return fileList; + } catch (ex) { + return fileList; + } + }; + private _getSchemas = async (): Promise => { if (!(await this.exists())) { throw new Error(`${this.dir} is not a valid path`); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index 02d0e0eb82..b4e3e03259 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -26,6 +26,7 @@ const BotStructureTemplate = { }, formDialogs: 'form-dialogs/${FORMDIALOGNAME}', skillManifests: 'manifests/${MANIFESTFILENAME}', + botProject: '${BOTNAME}.botproj', }; const templateInterpolate = (str: string, obj: { [key: string]: string }) => @@ -91,6 +92,7 @@ export const defaultFilePath = (botName: string, defaultLocale: string, filename if (fileType === FileExtensions.DialogSchema) { TemplatePath = isRootFile ? BotStructureTemplate.dialogSchema : BotStructureTemplate.dialogs.dialogSchema; } + return templateInterpolate(TemplatePath, { BOTNAME, DIALOGNAME, @@ -106,6 +108,8 @@ export const serializeFiles = async (fileStorage, rootPath, botName) => { templateInterpolate(BotStructureTemplate.lu, { LOCALE: '*', BOTNAME: '*' }), templateInterpolate(BotStructureTemplate.qna, { LOCALE: '*', BOTNAME: '*' }), templateInterpolate(BotStructureTemplate.dialogSchema, { BOTNAME: '*' }), + templateInterpolate(BotStructureTemplate.dialogSchema, { BOTNAME: '*' }), + templateInterpolate(BotStructureTemplate.botProject, { BOTNAME: '*' }), ]; for (const pattern of entryPatterns) { const paths = await fileStorage.glob(pattern, rootPath); diff --git a/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj new file mode 100644 index 0000000000..17932761e7 --- /dev/null +++ b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} diff --git a/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj new file mode 100644 index 0000000000..3ca0f8a01a --- /dev/null +++ b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "name": "", + "workspace": "", + "skills": {} +} From f105aea7ef4fd07c015ac5d3af984c04a1398069 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 2 Oct 2020 12:43:07 -0700 Subject: [PATCH 013/100] Migrate to old desing page and project tree Signed-off-by: Srinaath Ravichandran --- .../components/ProjectTree/ProjectTree.tsx | 331 ++++++++---------- .../src/components/ProjectTree/treeItem.tsx | 104 ++---- .../client/src/pages/design/DesignPage.tsx | 221 +++++------- 3 files changed, 256 insertions(+), 400 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index 7ba56d52d5..45d4910a26 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -2,20 +2,26 @@ // Licensed under the MIT License. /** @jsx jsx */ -import React, { useCallback, useState, Fragment } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { jsx, css } from '@emotion/core'; +import { + GroupedList, + IGroup, + IGroupHeaderProps, + IGroupRenderProps, + IGroupedList, +} from 'office-ui-fabric-react/lib/GroupedList'; import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox'; import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone'; import cloneDeep from 'lodash/cloneDeep'; import formatMessage from 'format-message'; import { DialogInfo, ITrigger } from '@bfc/shared'; -import { Resizable, ResizeCallback } from 're-resizable'; import debounce from 'lodash/debounce'; import { useRecoilValue } from 'recoil'; +import { IGroupedListStyles } from 'office-ui-fabric-react/lib/GroupedList'; import { ISearchBoxStyles } from 'office-ui-fabric-react/lib/SearchBox'; -import { dispatcherState, userSettingsState } from '../../recoilModel'; -import { botProjectSpaceSelector } from '../../recoilModel/selectors'; +import { dispatcherState } from '../../recoilModel'; import { createSelectedPath, getFriendlyName } from '../../utils/dialogUtil'; import { containUnsupportedTriggers, triggerNotSupported } from '../../utils/dialogValidator'; @@ -23,6 +29,13 @@ import { TreeItem } from './treeItem'; // -------------------- Styles -------------------- // +const groupListStyle: Partial = { + root: { + width: '100%', + boxSizing: 'border-box', + }, +}; + const searchBox: ISearchBoxStyles = { root: { borderBottom: '1px solid #edebe9', @@ -34,34 +47,36 @@ const searchBox: ISearchBoxStyles = { const root = css` width: 100%; height: 100%; - border-right: 1px solid #c4c4c4; box-sizing: border-box; overflow-y: auto; overflow-x: hidden; .ms-List-cell { - min-height: 24px; + min-height: 36px; } `; -const summaryStyle = css` - display: flex; - padding-left: 12px; - padding-top: 12px; -`; - // -------------------- ProjectTree -------------------- // -export type TreeLink = { - displayName: string; - isRoot: boolean; - warningContent?: string; - projectId: string; - dialogName?: string; - trigger?: number; -}; +function createGroupItem(dialog: DialogInfo, currentId: string, position: number, warningContent: string): IGroup { + return { + key: dialog.id, + name: dialog.displayName, + level: 1, + startIndex: position, + count: dialog.triggers.length, + hasMoreData: true, + isCollapsed: dialog.id !== currentId, + data: { ...dialog, warningContent }, + }; +} -function getTriggerName(trigger: ITrigger) { - return trigger.displayName || getFriendlyName({ $kind: trigger.type }); +function createItem(trigger: ITrigger, index: number, warningContent: string) { + return { + ...trigger, + index, + warningContent, + displayName: trigger.displayName || getFriendlyName({ $kind: trigger.type }), + }; } function sortDialog(dialogs: DialogInfo[]) { @@ -77,218 +92,148 @@ function sortDialog(dialogs: DialogInfo[]) { }); } +function createItemsAndGroups( + dialogs: DialogInfo[], + dialogId: string, + filter: string +): { items: any[]; groups: IGroup[] } { + let position = 0; + const result = dialogs + .filter((dialog) => { + return dialog.displayName.toLowerCase().includes(filter.toLowerCase()); + }) + .reduce( + (result: { items: any[]; groups: IGroup[] }, dialog) => { + const warningContent = containUnsupportedTriggers(dialog); + result.groups.push(createGroupItem(dialog, dialogId, position, warningContent)); + position += dialog.triggers.length; + dialog.triggers.forEach((item, index) => { + const warningContent = triggerNotSupported(dialog, item); + result.items.push(createItem(item, index, warningContent)); + }); + return result; + }, + { items: [], groups: [] } + ); + return result; +} + interface IProjectTreeProps { - dialogId: string; - selected: any; dialogs: DialogInfo[]; - onDeleteDialog: any; - onDeleteTrigger: any; - onSelect: (link: TreeLink) => void; + dialogId: string; + selected: string; + onSelect: (id: string, selected?: string) => void; + onDeleteTrigger: (id: string, index: number) => void; + onDeleteDialog: (id: string) => void; } -const TYPE_TO_ICON_MAP = { - 'Microsoft.OnUnknownIntent': '', -}; - -type BotInProject = { - dialogs: any; - projectId: string; - name: string; -}; - export const ProjectTree: React.FC = (props) => { - const { onboardingAddCoachMarkRef, updateUserSettings } = useRecoilValue(dispatcherState); - const { dialogNavWidth: currentWidth } = useRecoilValue(userSettingsState); + const { onboardingAddCoachMarkRef } = useRecoilValue(dispatcherState); - const { selected, onSelect, onDeleteDialog: onDelete } = props; + const groupRef: React.RefObject = useRef(null); + const { dialogs, dialogId, selected, onSelect, onDeleteTrigger, onDeleteDialog } = props; const [filter, setFilter] = useState(''); const delayedSetFilter = debounce((newValue) => setFilter(newValue), 1000); const addMainDialogRef = useCallback((mainDialog) => onboardingAddCoachMarkRef({ mainDialog }), []); - const projectCollection = useRecoilValue(botProjectSpaceSelector); - const renderBotHeader = (bot: BotInProject, hasWarnings: boolean) => { - const link: TreeLink = { - displayName: bot.name, - projectId: bot.projectId, - isRoot: true, - warningContent: hasWarnings ? formatMessage('This bot has warnings') : undefined, - }; + const sortedDialogs = useMemo(() => { + return sortDialog(dialogs); + }, [dialogs]); - return ( - - - - ); - }; - - const renderDialogHeader = (projectId: string, dialog: DialogInfo, warningContent: string) => { - const link: TreeLink = { - dialogName: dialog.id, - displayName: dialog.displayName, - isRoot: dialog.isRoot, - projectId: projectId, - warningContent, + const onRenderHeader = (props: IGroupHeaderProps) => { + const toggleCollapse = (): void => { + groupRef.current?.toggleCollapseAll(true); + props.onToggleCollapse?.(props.group!); + onSelect(props.group!.key); }; return ( - + ); }; - function renderTrigger(projectId: string, item: any, dialog: DialogInfo): React.ReactNode { - const link: TreeLink = { - displayName: item.displayName, - warningContent: item.warningContent, - trigger: item.index, - dialogName: dialog.id, - isRoot: false, - projectId: projectId, - }; - + function onRenderCell(nestingDepth?: number, item?: any): React.ReactNode { return ( onDeleteTrigger(dialogId, item.index)} + onSelect={() => onSelect(dialogId, createSelectedPath(item.index))} /> ); } + const onRenderShowAll = () => { + return null; + }; + const onFilter = (_e?: any, newValue?: string): void => { if (typeof newValue === 'string') { delayedSetFilter(newValue); } }; - const handleResize: ResizeCallback = (_e, _dir, _ref, d) => { - updateUserSettings({ dialogNavWidth: currentWidth + d.width }); - }; - - function filterMatch(scope: string) { - return scope.toLowerCase().includes(filter.toLowerCase()); - } - - function createDetailsTree(bot: BotInProject) { - const { projectId } = bot; - const dialogs = sortDialog(bot.dialogs); - - const filteredDialogs = - filter == null || filter.length === 0 - ? dialogs - : dialogs.filter( - (dialog) => - filterMatch(dialog.displayName) || dialog.triggers.some((trigger) => filterMatch(getTriggerName(trigger))) - ); - - return filteredDialogs.map((dialog: DialogInfo) => { - const triggerList = dialog.triggers - .filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr))) - .map((tr, index) => - renderTrigger( - projectId, - { ...tr, index, displayName: getTriggerName(tr), warningContent: triggerNotSupported(dialog, tr) }, - dialog - ) - ); - return ( -
- - {renderDialogHeader(projectId, dialog, containUnsupportedTriggers(dialog))} - - {triggerList} -
- ); - }); - } - - function createBotSubtree(bot: BotInProject) { - return ( -
- {renderBotHeader(bot, false)} - {createDetailsTree(bot)} -
- ); - } - - const projectTree = - projectCollection.length === 1 ? createDetailsTree(projectCollection[0]) : projectCollection.map(createBotSubtree); + const itemsAndGroups: { items: any[]; groups: IGroup[] } = createItemsAndGroups(sortedDialogs, dialogId, filter); return ( - - +
+ +
- - -
- {projectTree} - -
- - + { dialogNum: itemsAndGroups.groups.length } + )} + aria-live={'polite'} + /> + + } + styles={groupListStyle} + onRenderCell={onRenderCell} + /> +
+
); }; diff --git a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx index 9b7de9ae4b..fc32818c8b 100644 --- a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx +++ b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx @@ -15,10 +15,8 @@ import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuStyles } from 'office-ui-fabric-react/lib/ContextualMenu'; import { ICalloutContentStyles } from 'office-ui-fabric-react/lib/Callout'; -import { TreeLink } from './ProjectTree'; - // -------------------- Styles -------------------- // -const indent = 8; +const indent = 16; const itemText = (depth: number) => css` outline: none; :focus { @@ -40,15 +38,13 @@ const content = css` outline: none; display: flex; align-items: center; - justify-items: center; - height: 24px; label: ProjectTreeItem; `; -const leftIndent = (extraSpace: number) => css` +const leftIndent = css` height: 100%; - width: ${extraSpace + 8}px; + width: ${indent}px; `; const moreMenu: Partial = { @@ -81,13 +77,13 @@ const moreButton = (isActive: boolean): IButtonStyles => { }; }; -const navItem = (isActive: boolean) => css` +const navItem = (isActive: boolean, isSubItemActive: boolean) => css` width: 100%; position: relative; - height: 24px; + height: 36px; font-size: 12px; - color: ${isActive ? '#ffffff' : '#545454'}; - background: ${isActive ? '#0078d4' : 'transparent'}; + color: #545454; + background: ${isActive && !isSubItemActive ? '#f2f2f2' : 'transparent'}; font-weight: ${isActive ? FontWeights.semibold : FontWeights.regular}; &:hover { color: #545454; @@ -114,13 +110,13 @@ const navItem = (isActive: boolean) => css` } `; -export const overflowSet = (depth: number) => css` +export const overflowSet = css` width: 100%; height: 100%; - padding-left: ${depth * 12}px; + padding-left: 12px; padding-right: 12px; box-sizing: border-box; - line-height: 24px; + line-height: 36px; justify-content: space-between; display: flex; justify-content: space-between; @@ -135,43 +131,38 @@ const warningIcon = { // -------------------- TreeItem -------------------- // interface ITreeItemProps { - link: TreeLink; - isActive?: boolean; + link: any; + isActive: boolean; isSubItemActive?: boolean; depth: number | undefined; - onDelete?: (link: TreeLink) => void; - onSelect: (link: TreeLink) => void; - icon?: string; - dialogName?: string; - showProps?: boolean; - extraSpace?: number; + onDelete: (id: string) => void; + onSelect: (id: string) => void; } -const onRenderItem = (extraSpace: number) => (item: IOverflowSetItemProps) => { +const onRenderItem = (item: IOverflowSetItemProps) => { const warningContent = formatMessage( 'This trigger type is not supported by the RegEx recognizer and will not be fired.' ); return (
-
+
{item.warningContent ? ( ) : ( -
+
)} - {item.icon != null && ( + {item.depth !== 0 && ( (item: IOverflowSetItemProps) => { ); }; -const onRenderOverflowButton = (showIcon: boolean, isActive: boolean) => { +const onRenderOverflowButton = (isRoot: boolean, isActive: boolean) => { const moreLabel = formatMessage('Actions'); + const showIcon = !isRoot; return (overflowItems) => { return showIcon ? ( @@ -213,43 +205,18 @@ const onRenderOverflowButton = (showIcon: boolean, isActive: boolean) => { }; export const TreeItem: React.FC = (props) => { - const { link, isActive, depth, onDelete, onSelect, icon, dialogName } = props; - - const a11yLabel = `${dialogName ?? '$Root'}_${link.displayName}`; - - const overflowMenu: { key: string; name: string; onClick: () => void }[] = []; - - if (onDelete != null) { - overflowMenu.push({ - key: 'delete', - name: formatMessage('Delete'), - onClick: () => onDelete(link), - }); - } - - if (props.showProps) { - overflowMenu.push({ - key: 'props', - name: formatMessage('Properties'), - onClick: () => onSelect(link), - }); - } - - const linkString = `${link.projectId}_DialogTreeItem${link.dialogName}_${link.trigger ?? ''}`; + const { link, isActive, isSubItemActive, depth, onDelete, onSelect } = props; return (
{ - onSelect(link); + onSelect(link.id); }} onKeyDown={(e) => { if (e.key === 'Enter') { - onSelect(link); + onSelect(link.id); } }} > @@ -257,21 +224,26 @@ export const TreeItem: React.FC = (props) => { //In 8.0 the OverflowSet will no longer be wrapped in a FocusZone //remove this at that time doNotContainWithinFocusZone - css={overflowSet(depth ?? 0)} - data-testid={linkString} + css={overflowSet} + data-testid={`DialogTreeItem${link.id}`} items={[ { - key: linkString, + key: link.id, depth, - icon, ...link, }, ]} - overflowItems={overflowMenu} + overflowItems={[ + { + key: 'delete', + name: formatMessage('Delete'), + onClick: () => onDelete(link.id), + }, + ]} role="row" styles={{ item: { flex: 1 } }} - onRenderItem={onRenderItem(props.extraSpace ?? 0)} - onRenderOverflowButton={onRenderOverflowButton(!link.isRoot, !!isActive)} + onRenderItem={onRenderItem} + onRenderOverflowButton={onRenderOverflowButton(link.isRoot, isActive)} />
); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index f3563ed189..3969cc7eb1 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -8,12 +8,13 @@ import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcru import formatMessage from 'format-message'; import { globalHistory, RouteComponentProps } from '@reach/router'; import get from 'lodash/get'; -import { DialogInfo, PromptTab, getEditorAPI, registerEditorAPI } from '@bfc/shared'; +import { DialogInfo, PromptTab, getEditorAPI, registerEditorAPI, FieldNames } from '@bfc/shared'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { JsonEditor } from '@bfc/code-editor'; import { EditorExtension, useTriggerApi, PluginConfig } from '@bfc/extension-client'; import { useRecoilValue } from 'recoil'; +import { LeftRightSplit } from '../../components/Split/LeftRightSplit'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { TestController } from '../../components/TestController/TestController'; import { DialogDeleting } from '../../constants'; @@ -47,11 +48,11 @@ import { showCreateDialogModalState, showAddSkillDialogModalState, localeState, - botProjectSpaceSelector, } from '../../recoilModel'; import ImportQnAFromUrlModal from '../knowledge-base/ImportQnAFromUrlModal'; import { triggerNotSupported } from '../../utils/dialogValidator'; import { undoFunctionState, undoVersionState } from '../../recoilModel/undo/history'; +import { decodeDesignerPathToArrayPath } from '../../utils/convertUtils/designerPathEncoder'; import { WarningMessage } from './WarningMessage'; import { @@ -109,7 +110,7 @@ const getTabFromFragment = () => { const DesignPage: React.FC> = (props) => { const { location, dialogId, projectId = '' } = props; const userSettings = useRecoilValue(userSettingsState); - const botProjectsSpace = useRecoilValue(botProjectSpaceSelector); + const schemas = useRecoilValue(schemasState(projectId)); const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); const displaySkillManifest = useRecoilValue(displaySkillManifestState(projectId)); @@ -120,7 +121,6 @@ const DesignPage: React.FC x.id === props.dialogId)?.content, + params.get('selected') || '' + ); const [triggerModalVisible, setTriggerModalVisibility] = useState(false); const [dialogJsonVisible, setDialogJsonVisibility] = useState(false); const [importQnAModalVisibility, setImportQnAModalVisibility] = useState(false); @@ -161,10 +159,6 @@ const DesignPage: React.FC { - console.log(botProjectsSpace); - }, [botProjectsSpace]); - useEffect(() => { const currentDialog = dialogs.find(({ id }) => id === dialogId); if (currentDialog) { @@ -195,8 +189,9 @@ const DesignPage: React.FC ({ ...acc, [id]: content }), {}); - const selected = params.get('selected') ?? ''; - const focused = params.get('focused') ?? ''; + const dialogData = getDialogData(dialogMap, dialogId); + const selected = decodeDesignerPathToArrayPath(dialogData, params.get('selected') ?? ''); + const focused = decodeDesignerPathToArrayPath(dialogData, params.get('focused') ?? ''); //make sure focusPath always valid const data = getDialogData(dialogMap, dialogId, getFocusPath(selected, focused)); @@ -321,73 +316,6 @@ const DesignPage: React.FC { - addRemoteSkillToBotProject( - 'https://onenote-dev.azurewebsites.net/manifests/OneNoteSync-2-1-preview-1-manifest.json', - 'OneNoteSyncer', - 'remote' - ); - }, - }, - { - 'data-testid': 'AddLocalSkill', - key: 'addLocalSkill', - text: formatMessage(`Add local skill from path`, { - displayName: currentDialog?.displayName ?? '', - }), - onClick: () => { - addExistingSkillToBotProject('/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync'); - }, - }, - { - 'data-testid': 'createNewSkill', - key: 'createNewSkill', - text: formatMessage(`Create new Skill`, { - displayName: currentDialog?.displayName ?? '', - }), - onClick: () => { - addNewSkillToBotProject({ - name: 'newers-bot', - description: '', - schemaUrl: '', - location: '/Users/srravich/Desktop/samples', - templateId: 'InterruptionSample', - locale: appLocale, - qnaKbUrls: [], - }); - }, - }, - { - 'data-testid': 'removeSkillAtIndex', - key: 'removeSkillAtIndex', - text: formatMessage(`Remove a skill`, { - displayName: currentDialog?.displayName ?? '', - }), - onClick: () => { - const matchedProject: any = botProjectsSpace[botProjectsSpace.length - 1]; - removeSkillFromBotProject(matchedProject.projectId); - }, - }, - { - 'data-testid': 'replaceSkillAtIndex', - key: 'replaceSkillAtIndex', - text: formatMessage(`Replace a skill`, { - displayName: currentDialog?.displayName ?? '', - }), - onClick: () => { - const matchedProject: any = botProjectsSpace[botProjectsSpace.length - 1]; - replaceSkillInBotProject( - matchedProject.projectId, - '/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync' - ); - }, - }, ], }, }, @@ -632,8 +560,15 @@ const DesignPage: React.FC d.id === dialogId); + if (dialogId && dialog) { + const url = `/bot/${projectId}/knowledge-base/${dialogId}`; + const triggers = get(dialog, FieldNames.Events, []); + if (triggers.some((t) => t.type === qnaMatcherKey)) { + navigateTo(url); + } else { + createTrigger(dialogId, formData, url); + } // import qna from urls if (urls.length > 0) { await importQnAFromUrls({ id: `${dialogId}.${locale}`, urls, projectId }); @@ -657,64 +592,68 @@ const DesignPage: React.FC
- handleSelect(projectId, ...props)} - /> -
-
- - -
- -
-
- {breadcrumbItems} - {dialogJsonVisible ? ( - { - updateDialog({ id: currentDialog.id, content: data, projectId }); - }} - /> - ) : withWarning ? ( - warningIsVisible && ( - { - setWarningIsVisible(false); - }} - onOk={() => navigateTo(`/bot/${projectId}/knowledge-base/all`)} - /> - ) - ) : ( - - setFlowEditorFocused(false)} - onFocus={() => setFlowEditorFocused(true)} - /> + + handleSelect(projectId, ...props)} + /> +
+
+ + +
+ +
+ +
+ {breadcrumbItems} + {dialogJsonVisible ? ( + { + updateDialog({ id: currentDialog.id, content: data, projectId }); + }} + /> + ) : withWarning ? ( + warningIsVisible && ( + { + setWarningIsVisible(false); + }} + onOk={() => navigateTo(`/bot/${projectId}/knowledge-base/all`)} + /> + ) + ) : ( + + setFlowEditorFocused(false)} + onFocus={() => setFlowEditorFocused(true)} + /> + + )} +
+ + - )} +
- - - -
- -
+ +
+
}> {showCreateDialogModal && ( From 9aa46ca1775fd6c53c724355563a040870ae2dba Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 2 Oct 2020 12:52:23 -0700 Subject: [PATCH 014/100] Revert usage of React 16.13.1 to 16.13.0 Signed-off-by: Srinaath Ravichandran --- Composer/packages/adaptive-flow/package.json | 4 +- Composer/packages/adaptive-form/package.json | 4 +- Composer/packages/client/package.json | 2 +- .../src/recoilModel/selectors/project.ts | 1 - .../packages/extension-client/package.json | 4 +- Composer/packages/intellisense/package.json | 2 +- .../packages/lib/code-editor/package.json | 6 +- Composer/packages/lib/shared/package.json | 4 +- Composer/packages/lib/ui-shared/package.json | 4 +- .../packages/server/src/locales/en-US.json | 2397 +---------------- .../packages/ui-plugins/composer/package.json | 6 +- .../ui-plugins/cross-trained/package.json | 4 +- .../packages/ui-plugins/example/package.json | 4 +- Composer/packages/ui-plugins/lg/package.json | 4 +- .../packages/ui-plugins/luis/package.json | 4 +- .../packages/ui-plugins/prompts/package.json | 4 +- .../ui-plugins/schema-editor/package.json | 4 +- .../ui-plugins/select-dialog/package.json | 4 +- .../select-skill-dialog/package.json | 4 +- Composer/yarn.lock | 8 +- 20 files changed, 39 insertions(+), 2435 deletions(-) diff --git a/Composer/packages/adaptive-flow/package.json b/Composer/packages/adaptive-flow/package.json index adde0e7f0e..2d69e0b49d 100644 --- a/Composer/packages/adaptive-flow/package.json +++ b/Composer/packages/adaptive-flow/package.json @@ -42,7 +42,7 @@ }, "peerDependencies": { "format-message": "^6.2.3", - "react": "16.13.1" + "react": "16.13.0" }, "devDependencies": { "@bfc/test-utils": "*", @@ -50,7 +50,7 @@ "@types/react": "16.9.23", "format-message": "^6.2.3", "json-loader": "^0.5.7", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1", "ts-loader": "7.0.4", "tsconfig-paths-webpack-plugin": "^3.2.0" diff --git a/Composer/packages/adaptive-form/package.json b/Composer/packages/adaptive-form/package.json index 2a91119915..18484c542d 100644 --- a/Composer/packages/adaptive-form/package.json +++ b/Composer/packages/adaptive-form/package.json @@ -26,7 +26,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -37,7 +37,7 @@ "@types/lodash": "^4.14.149", "@types/react": "16.9.23", "format-message": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index b9417f2435..860313b333 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -51,7 +51,7 @@ "query-string": "^6.8.2", "react-measure": "^2.3.0", "re-resizable": "^6.3.2", - "react": "16.13.1", + "react": "16.13.0", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.3", "react-dom": "16.13.1", diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index 9f976b162f..c568cf99c9 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -39,7 +39,6 @@ export const botProjectSpaceSelector = selector({ const botNameId = get(botNameIdentifierState(projectId)); return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); - console.log(result); return result; }, }); diff --git a/Composer/packages/extension-client/package.json b/Composer/packages/extension-client/package.json index 0948d57580..8e67eb418e 100644 --- a/Composer/packages/extension-client/package.json +++ b/Composer/packages/extension-client/package.json @@ -11,13 +11,13 @@ "clean": "rimraf lib" }, "peerDependencies": { - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", "@types/react": "16.9.23", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1", "rimraf": "^3.0.2", "typescript": "3.9.2" diff --git a/Composer/packages/intellisense/package.json b/Composer/packages/intellisense/package.json index 0dcb7ef5b1..dcb25fb93a 100644 --- a/Composer/packages/intellisense/package.json +++ b/Composer/packages/intellisense/package.json @@ -10,7 +10,7 @@ }, "peerDependencies": { "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/lib/code-editor/package.json b/Composer/packages/lib/code-editor/package.json index 9a149f06b9..3f8697b6b1 100644 --- a/Composer/packages/lib/code-editor/package.json +++ b/Composer/packages/lib/code-editor/package.json @@ -24,7 +24,7 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -43,7 +43,7 @@ "mini-css-extract-plugin": "^0.6.0", "postcss-loader": "^3.0.0", "react-dom": "16.13.1", - "react": "16.13.1", + "react": "16.13.0", "rimraf": "^2.6.3", "style-loader": "^0.23.1", "ts-loader": "7.0.4", @@ -66,4 +66,4 @@ "reconnecting-websocket": "^3.2.2", "vscode-ws-jsonrpc": "^0.1.1" } -} \ No newline at end of file +} diff --git a/Composer/packages/lib/shared/package.json b/Composer/packages/lib/shared/package.json index c9a10931f0..b9f769e089 100644 --- a/Composer/packages/lib/shared/package.json +++ b/Composer/packages/lib/shared/package.json @@ -21,7 +21,7 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -32,7 +32,7 @@ "autoprefixer": "^9.5.1", "copyfiles": "^2.1.0", "office-ui-fabric-react": "^7.88.1", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1", "rimraf": "^2.6.3" }, diff --git a/Composer/packages/lib/ui-shared/package.json b/Composer/packages/lib/ui-shared/package.json index 5ad003d57b..5e77e27442 100644 --- a/Composer/packages/lib/ui-shared/package.json +++ b/Composer/packages/lib/ui-shared/package.json @@ -24,7 +24,7 @@ "@emotion/core": "^10.0.27", "@emotion/styled": "^10.0.27", "@bfc/extension-client": "*", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 5c8444ead3..9e26dfeeb6 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -1,2396 +1 @@ -{ - "0_bytes_a1e1cdb3": { - "message": "0 Bytes" - }, - "5_minute_intro_7ea06d2b": { - "message": "5 Minute Intro" - }, - "ErrorInfo_part1": { - "message": "An error occured in the form editor!" - }, - "ErrorInfo_part2": { - "message": "This is likely due to malformed data or missing functionality in Composer." - }, - "ErrorInfo_part3": { - "message": "Try navigating to another node in the visual editor." - }, - "a_dialog_file_must_have_a_name_123ff67d": { - "message": "a dialog file must have a name" - }, - "a_minimap_gives_an_overview_of_your_source_code_fo_9a897f4f": { - "message": "A minimap gives an overview of your source code for quick navigation and code understanding." - }, - "a_newer_version_of_the_provisioning_scripts_has_be_85d3ad94": { - "message": "A newer version of the provisioning scripts has been found, and this project can be updated to the latest." - }, - "a_profile_with_that_name_already_exists_7c559958": { - "message": "A profile with that name already exists." - }, - "a_subscription_key_is_created_when_you_create_a_qn_37a6926f": { - "message": "A subscription key is created when you create a QnA Maker resource." - }, - "a_valid_url_should_start_with_http_or_https_d24b3591": { - "message": "A valid url should start with http:// or https://" - }, - "about_70c18bba": { - "message": "About" - }, - "access_external_resources_7e37fe21": { - "message": "Access external resources" - }, - "action_created_f80af9a0": { - "message": "Action created" - }, - "action_deleted_a10c002b": { - "message": "Action deleted" - }, - "action_focused_400ee4af": { - "message": "Action focused" - }, - "action_unfocused_18a2800e": { - "message": "Action unfocused" - }, - "actions_copied_2821ab27": { - "message": "Actions copied" - }, - "actions_created_6c5acc11": { - "message": "Actions created" - }, - "actions_cut_929f4c37": { - "message": "Actions cut" - }, - "actions_define_b_how_the_bot_responds_b_to_a_certa_890a71f4": { - "message": "Actions define how the bot responds to a certain trigger." - }, - "actions_deleted_355c359a": { - "message": "Actions deleted" - }, - "actions_disabled_c1accfb6": { - "message": "Actions disabled" - }, - "actions_efcde10d": { - "message": "Actions" - }, - "actions_enabled_8e9ec02d": { - "message": "Actions enabled" - }, - "actions_moved_d7777b29": { - "message": "Actions moved" - }, - "activities_125193f7": { - "message": "Activities" - }, - "activity_13915493": { - "message": "Activity" - }, - "activity_received_2f20fa9d": { - "message": "Activity received" - }, - "adaptive_dialog_61a05dde": { - "message": "Adaptive dialog" - }, - "add_8523c19b": { - "message": "Add" - }, - "add_a_new_key_5c208c29": { - "message": "Add a new key" - }, - "add_a_new_skill_dialog_aaaafa9c": { - "message": "Add a new Skill Dialog" - }, - "add_a_new_trigger_a82d3db8": { - "message": "Add a new trigger" - }, - "add_a_new_value_24ca14ac": { - "message": "Add a new value" - }, - "add_a_publish_profile_fca2263a": { - "message": "Add a publish profile" - }, - "add_a_skill_46d2b71c": { - "message": "Add a skill" - }, - "add_a_welcome_message_9e1480b2": { - "message": "Add a welcome message" - }, - "add_additional_url_bdfac25d": { - "message": "Add additional URL" - }, - "add_alternative_phrasing_47234f77": { - "message": "add alternative phrasing" - }, - "add_an_intent_trigger_a9acc149": { - "message": "Add an intent trigger" - }, - "add_custom_runtime_6b73dc44": { - "message": "Add custom runtime" - }, - "add_language_75dee49e": { - "message": "Add language" - }, - "add_multiple_comma_separated_synonyms_2639283f": { - "message": "Add multiple comma-separated synonyms" - }, - "add_new_dialog_25f12c9": { - "message": "Add new dialog" - }, - "add_new_extension_19b82b77": { - "message": "Add new extension" - }, - "add_new_knowledge_base_on_displayname_7f44609c": { - "message": " Add new knowledge base on { displayName }" - }, - "add_new_profile_47b225e6": { - "message": "Add new profile" - }, - "add_new_propertyname_bedf7dc6": { - "message": "Add new { propertyName }" - }, - "add_new_trigger_on_displayname_2aaf6e4c": { - "message": "Add new trigger on { displayName }" - }, - "add_new_validation_rule_here_eb675ccf": { - "message": "Add new validation rule here" - }, - "add_qna_pair_bcb1624": { - "message": "Add QnA Pair" - }, - "add_some_example_phrases_to_trigger_this_intent_pl_568eaf51": { - "message": "> add some example phrases to trigger this intent:\n> - please tell me the weather\n> - what is the weather like in '{'city=Seattle'}'\n\n> entity definitions:\n> @ ml city" - }, - "add_some_expected_user_responses_please_remind_me__31dc5c07": { - "message": "> add some expected user responses:\n> - Please remind me to '{'itemTitle=buy milk'}'\n> - remind me to '{'itemTitle'}'\n> - add '{'itemTitle'}' to my todo list\n>\n> entity definitions:\n> @ ml itemTitle\n" - }, - "add_welcome_message_49d9ded9": { - "message": "Add welcome message" - }, - "advanced_events_2cbfa47d": { - "message": "Advanced Events" - }, - "all_4321c3a1": { - "message": "All" - }, - "all_language_generation_files_ef778f24": { - "message": "all language generation files" - }, - "all_language_understanding_files_714b33eb": { - "message": "all language understanding files" - }, - "all_profiles_8321b1aa": { - "message": "All profiles" - }, - "all_qna_files_349d7fe1": { - "message": "all qna files" - }, - "an_authoring_key_is_created_automatically_when_you_21cf77aa": { - "message": "An authoring key is created automatically when you create a LUIS account." - }, - "answer_4620913f": { - "message": "Answer" - }, - "answer_is_answer_799483f0": { - "message": "Answer is { answer }" - }, - "any_constant_or_expression_to_evaluate_ba2017b1": { - "message": "Any constant or expression to evaluate." - }, - "any_or_expression_acad7d37": { - "message": "Any or expression" - }, - "append_choices_35c45a2d": { - "message": "Append choices" - }, - "application_language_87691b6": { - "message": "Application Language" - }, - "application_language_f100f3e0": { - "message": "Application language" - }, - "application_settings_39e840c6": { - "message": "Application Settings" - }, - "application_updates_bdf5f8b6": { - "message": "Application Updates" - }, - "apr_9_2020_3c8b47d7": { - "message": "Apr 9, 2020" - }, - "are_you_sure_you_want_to_exit_the_onboarding_produ_c2de1b23": { - "message": "Are you sure you want to exit the Onboarding Product Tour? You can restart the tour in the onboarding settings." - }, - "are_you_sure_you_want_to_stop_current_runtime_and__a5f883b2": { - "message": "Are you sure you want to stop current runtime and replace them?" - }, - "array_643947ee": { - "message": "Array" - }, - "array_constant_8829b2ec": { - "message": "Array constant." - }, - "array_or_expression_c52b2ecc": { - "message": "Array or expression" - }, - "array_or_expression_to_evaluate_87d9328b": { - "message": "Array or expression to evaluate." - }, - "ask_a_question_92ef7e0c": { - "message": "Ask a question" - }, - "attachment_input_e0ece49c": { - "message": "Attachment Input" - }, - "australia_6a25c95b": { - "message": "australia" - }, - "authoring_canvas_18802e39": { - "message": "Authoring canvas" - }, - "authoring_region_to_use_e_g_westus_westeurope_aust_d43d5245": { - "message": "Authoring region to use (e.g. westus, westeurope, australiaeast)" - }, - "authoring_region_to_use_westus_qna_maker_resource__4588c2f9": { - "message": "Authoring region to use (westus) (QnA maker resource location)" - }, - "auto_update_86eb33b0": { - "message": "Auto update" - }, - "available_skills_95c114ac": { - "message": "Available Skills" - }, - "been_used_5daccdb2": { - "message": "Been used" - }, - "begin_a_new_dialog_60249bd8": { - "message": "Begin a new dialog" - }, - "begin_a_remote_skill_dialog_93e47189": { - "message": "Begin a remote skill dialog." - }, - "begin_dialog_12e2becf": { - "message": "Begin Dialog" - }, - "begin_dialog_event_285bc650": { - "message": "Begin dialog event" - }, - "begindialog_a5594562": { - "message": "BeginDialog" - }, - "ben_brown_99c12d19": { - "message": "Ben Brown" - }, - "boolean_6000988a": { - "message": "Boolean" - }, - "boolean_condition_b65450ca": { - "message": "Boolean condition" - }, - "boolean_constant_7d3fcbf6": { - "message": "Boolean constant" - }, - "boolean_constant_8d950af8": { - "message": "Boolean constant." - }, - "boolean_constant_or_expression_to_evaluate_86d77849": { - "message": "Boolean constant or expression to evaluate." - }, - "boolean_field_602934c9": { - "message": "boolean field" - }, - "boolean_or_expression_6bd88208": { - "message": "Boolean or expression" - }, - "boolean_value_98d39ea1": { - "message": "Boolean value." - }, - "bot_asks_5e9f0202": { - "message": "Bot Asks" - }, - "bot_framework_composer_enables_developers_and_mult_ce0e42a9": { - "message": "Bot Framework Composer enables developers and multi-disciplinary teams to build all kinds of conversational experiences, using the latest components from the Bot Framework: SDK, LG, LU, and declarative file formats, all without writing code." - }, - "bot_framework_composer_fae721be": { - "message": "Bot Framework Composer" - }, - "bot_framework_composer_icon_gray_fa72d3d6": { - "message": "bot framework composer icon gray" - }, - "bot_framework_composer_is_a_visual_authoring_canva_c3947d91": { - "message": "Bot Framework Composer is a visual authoring canvas for building bots and other types of conversational application with the Microsoft Bot Framework technology stack. With Composer you will find everything you need to build a modern, state-of-the-art conversational experience." - }, - "bot_framework_composer_is_an_open_source_visual_au_2be2e02b": { - "message": "Bot Framework Composer is an open-source visual authoring canvas for developers and multi-disciplinary teams to build bots. Composer integrates LUIS and QnA Maker, and allows sophisticated composition of bot replies using language generation." - }, - "bot_framework_provides_the_most_comprehensive_expe_e34a7f5d": { - "message": "Bot Framework provides the most comprehensive experience for building conversational applications." - }, - "bot_language_6cf30c2": { - "message": "Bot language" - }, - "bot_language_active_7cf9dc78": { - "message": "Bot language (active)" - }, - "bot_name_is_botname_a28c2d05": { - "message": "Bot name is { botName }" - }, - "bot_responses_4617b4a2": { - "message": "Bot responses" - }, - "bot_responses_c4e63601": { - "message": "Bot Responses" - }, - "bot_settings_3751e1b2": { - "message": "Bot settings" - }, - "bot_settings_ce2783e4": { - "message": "Bot Settings" - }, - "branch_if_else_391e5681": { - "message": "Branch: If/else" - }, - "branch_if_else_992cf9bf": { - "message": "Branch: If/Else" - }, - "branch_if_else_f6a36f1d": { - "message": "Branch: if/else" - }, - "branch_switch_multiple_options_95c6a326": { - "message": "Branch: Switch (multiple options)" - }, - "break_out_of_loop_ab30157c": { - "message": "Break out of loop" - }, - "build_your_first_bot_f9c3e427": { - "message": "Build your first bot" - }, - "bytes_74acea97": { - "message": "Bytes" - }, - "calculating_17b21be7": { - "message": "Calculating..." - }, - "cancel_all_active_dialogs_335b1623": { - "message": "Cancel all active dialogs" - }, - "cancel_all_dialogs_32144c45": { - "message": "Cancel All Dialogs" - }, - "cancel_caeb1e68": { - "message": "Cancel" - }, - "cancel_dialog_event_cc671dee": { - "message": "Cancel dialog event" - }, - "change_recognizer_3145b93d": { - "message": "Change Recognizer" - }, - "check_for_updates_and_install_them_automatically_50337340": { - "message": "Check for updates and install them automatically." - }, - "choice_input_f75a2353": { - "message": "Choice Input" - }, - "choice_name_fe8411f4": { - "message": "Choice Name" - }, - "choose_a_location_for_your_new_bot_project_e979f2d5": { - "message": "Choose a location for your new bot project." - }, - "choose_how_to_create_your_bot_a97f7b3e": { - "message": "Choose how to create your bot" - }, - "choose_one_2c4277df": { - "message": "Choose One" - }, - "chris_whitten_11df1f35": { - "message": "Chris Whitten" - }, - "click_on_the_b_add_b_button_in_the_toolbar_and_sel_4daf351a": { - "message": "Click on the Add button in the toolbar, and select Add a new trigger. In the Create a trigger wizard, set the Trigger Type to Intent recognized and configure the Trigger Name and Trigger Phrases. Then add actions in the Visual Editor." - }, - "click_the_b_add_b_button_in_the_toolbar_and_select_79001156": { - "message": "Click the Add button in the toolbar, and select Add a new trigger from the dropdown menu." - }, - "click_to_sort_by_file_type_1b0c9bd": { - "message": "Click to sort by file type" - }, - "close_d634289d": { - "message": "Close" - }, - "collapse_34080b4d": { - "message": "Collapse" - }, - "collapse_navigation_17228b95": { - "message": "Collapse Navigation" - }, - "comment_7ef1428e": { - "message": "Comment" - }, - "component_stacktrace_e24b1983": { - "message": "Component Stacktrace:" - }, - "composer_cannot_yet_translate_your_bot_automatical_2d54081b": { - "message": "Composer cannot yet translate your bot automatically.\nTo create a translation manually, Composer will create a copy of your bot’s content with the name of the additional language. This content can then be translated without affecting the original bot logic or flow and you can switch between languages to ensure the responses are correctly and appropriately translated." - }, - "composer_introduction_98a93701": { - "message": "Composer introduction" - }, - "composer_is_up_to_date_9118257d": { - "message": "Composer is up to date." - }, - "composer_logo_ba2048a0": { - "message": "Composer Logo" - }, - "composer_needs_net_core_sdk_46e2a8ae": { - "message": "Composer needs .NET Core SDK" - }, - "composer_will_restart_88ee8dc3": { - "message": "Composer will restart." - }, - "composer_will_update_the_next_time_you_close_the_a_d74264a1": { - "message": "Composer will update the next time you close the app." - }, - "conditionalselector_ed2031f0": { - "message": "ConditionalSelector" - }, - "configure_composer_to_start_your_bot_using_runtime_fe37dadf": { - "message": "Configure Composer to start your bot using runtime code you can customize and control." - }, - "configures_default_language_model_to_use_if_there__f09f1acd": { - "message": "Configures default language model to use if there is no culture code in the file name (Default: en-us)" - }, - "confirm_6556b3a6": { - "message": "Confirm" - }, - "confirm_choices_db8e99fb": { - "message": "Confirm Choices" - }, - "confirm_input_bf996e7a": { - "message": "Confirm Input" - }, - "confirmation_fec87d65": { - "message": "Confirmation" - }, - "confirmation_modal_must_have_a_title_b0816e0b": { - "message": "Confirmation modal must have a title." - }, - "congratulations_your_model_is_successfully_publish_52ebc297": { - "message": "Congratulations! Your model is successfully published." - }, - "connect_to_a_new_skill_34d582ab": { - "message": "Connect to a new skill" - }, - "connect_to_a_skill_53c9dff0": { - "message": "Connect to a skill" - }, - "connect_to_qna_knowledgebase_4b324132": { - "message": "Connect to QnA Knowledgebase" - }, - "continue_loop_22635585": { - "message": "Continue loop" - }, - "conversation_ended_a8bd37dd": { - "message": "Conversation ended" - }, - "conversation_invoked_e960884e": { - "message": "Conversation invoked" - }, - "conversationupdate_activity_9e94bff5": { - "message": "ConversationUpdate activity" - }, - "copy_9748f9f": { - "message": "Copy" - }, - "copy_content_for_translation_7affbcbb": { - "message": "Copy content for translation" - }, - "could_not_connect_to_storage_50411de0": { - "message": "Could not connect to storage." - }, - "could_not_init_plugin_1f1c29cd": { - "message": "Could not init plugin" - }, - "couldn_t_complete_the_update_a337a359": { - "message": "Couldn''t complete the update:" - }, - "create_132b3be1": { - "message": "Create" - }, - "create_a_bot_project_73e6ce33": { - "message": "Create a bot project" - }, - "create_a_condition_8686fd5": { - "message": "Create a condition" - }, - "create_a_dialog_9f6c8618": { - "message": "Create a dialog" - }, - "create_a_name_for_the_project_which_will_be_used_t_57e9b690": { - "message": "Create a name for the project which will be used to name the application: (projectname-environment-LUfilename)" - }, - "create_a_new_dialog_21d84b82": { - "message": "Create a new dialog" - }, - "create_a_new_skill_manifest_or_select_which_one_yo_a97e9616": { - "message": "Create a new skill manifest or select which one you want to edit" - }, - "create_a_trigger_40e74743": { - "message": "Create a trigger" - }, - "create_bot_from_template_or_scratch_92f0fefa": { - "message": "Create bot from template or scratch?" - }, - "create_copy_to_translate_bot_content_efc872c": { - "message": "Create copy to translate bot content" - }, - "create_folder_error_38aa86f5": { - "message": "Create Folder Error" - }, - "create_from_knowledge_base_qna_maker_7422486": { - "message": "Create from knowledge base (QnA Maker)" - }, - "create_from_qna_7aa9dcbb": { - "message": "Create from QnA" - }, - "create_from_scratch_485c3045": { - "message": "Create from scratch" - }, - "create_from_template_87e12c94": { - "message": "Create from template" - }, - "create_knowledge_base_db6d66c4": { - "message": "Create knowledge base" - }, - "create_knowledge_base_from_scratch_afe4d2a2": { - "message": "Create knowledge base from scratch" - }, - "create_new_empty_bot_21cf0ea3": { - "message": "Create new empty bot" - }, - "create_new_folder_19d3faa4": { - "message": "Create new folder" - }, - "create_new_knowledge_base_e14d07a5": { - "message": "Create New Knowledge Base" - }, - "create_or_edit_skill_manifest_8ad98da9": { - "message": "Create or edit skill manifest" - }, - "creating_your_knowledge_base_ef4f9872": { - "message": "Creating your knowledge base" - }, - "current_40c0812f": { - "message": " - Current" - }, - "custom_actions_5d396747": { - "message": "Custom Actions" - }, - "custom_events_d6f0e45b": { - "message": "Custom events" - }, - "custom_name_optional_60ee6a1a": { - "message": "Custom name (optional)" - }, - "custom_recognizer_951bab90": { - "message": "Custom recognizer" - }, - "cut_c8c92681": { - "message": "Cut" - }, - "data_loading_4c9bb9f6": { - "message": "Data loading..." - }, - "date_ee500367": { - "message": "Date" - }, - "date_modified_18beced9": { - "message": "Date modified" - }, - "date_modified_e1c8ac8f": { - "message": "Date Modified" - }, - "date_or_time_d30bcc7d": { - "message": "Date or time" - }, - "date_time_input_2416ffc1": { - "message": "Date Time Input" - }, - "debug_break_46cb5adb": { - "message": "Debug Break" - }, - "debug_break_870a75df": { - "message": "Debug break" - }, - "debugging_options_20e2e9da": { - "message": "Debugging options" - }, - "decrement_by_step_9b6c2fa3": { - "message": "decrement by { step }" - }, - "default_language_486a558d": { - "message": "Default language" - }, - "default_language_b11c37db": { - "message": "Default Language" - }, - "default_recognizer_9c06c1a3": { - "message": "Default recognizer" - }, - "define_conversation_objective_146d1cc6": { - "message": "Define conversation objective" - }, - "defined_in_475568fb": { - "message": "Defined in:" - }, - "definition_not_found_for_defname_7194fd07": { - "message": "Definition not found for { defName }" - }, - "delete_a6efa79d": { - "message": "Delete" - }, - "delete_a_property_f2d70f79": { - "message": "Delete a property" - }, - "delete_activity_6d881872": { - "message": "Delete activity" - }, - "delete_bot_73586104": { - "message": "Delete Bot" - }, - "delete_language_1527609d": { - "message": "Delete language" - }, - "delete_properties_8bc77b42": { - "message": "Delete Properties" - }, - "delete_properties_c49a7892": { - "message": "Delete properties" - }, - "delete_property_b3786fa0": { - "message": "Delete Property" - }, - "describe_your_skill_88554792": { - "message": "Describe your skill" - }, - "description_436c48d7": { - "message": "Description" - }, - "design_51b2812a": { - "message": "Design" - }, - "dialog_68ba69ba": { - "message": "(Dialog)" - }, - "dialog_cancelled_767b512a": { - "message": "Dialog cancelled" - }, - "dialog_data_61d5539b": { - "message": "Dialog data" - }, - "dialog_dialogid_not_found_5e8214c3": { - "message": "dialog { dialogId } not found" - }, - "dialog_events_f1b2e2a0": { - "message": "Dialog events" - }, - "dialog_input_schema_c7864fbd": { - "message": "Dialog input schema." - }, - "dialog_interface_398bc493": { - "message": "Dialog Interface" - }, - "dialog_management_2980578": { - "message": "Dialog management" - }, - "dialog_opened_f6392b1": { - "message": "Dialog opened" - }, - "dialog_output_schema_acfe2186": { - "message": "Dialog output schema." - }, - "dialog_started_912507c": { - "message": "Dialog started" - }, - "dialogfactory_missing_schema_5c3255c4": { - "message": "DialogFactory missing schema." - }, - "dialognum_plural_0_no_dialogs_1_one_dialog_other_d_1b86909b": { - "message": "{ dialogNum, plural,\n =0 {No dialogs}\n =1 {One dialog}\n other {# dialogs}\n} have been found.\n { dialogNum, select,\n 0 {}\n other {Press down arrow key to navigate the search results}\n}" - }, - "disable_a5c05db3": { - "message": "Disable" - }, - "display_lines_that_extends_beyond_the_width_of_the_9e500f3c": { - "message": "Display lines that extends beyond the width of the editor on the next line." - }, - "do_you_wish_to_continue_96469eaf": { - "message": "Do you wish to continue?" - }, - "does_not_exist_3a34b418": { - "message": "Does Not Exist" - }, - "done_1a4a010a": { - "message": "Done!" - }, - "done_54e3d4b6": { - "message": "Done" - }, - "download_now_and_install_when_you_close_composer_e241ed74": { - "message": "Download now and install when you close Composer." - }, - "downloading_bb6fb34b": { - "message": "Downloading..." - }, - "duplicate_dialog_name_824f9fce": { - "message": "Duplicate dialog name" - }, - "duplicate_fields_9fd0d3c2": { - "message": "duplicate fields" - }, - "duplicate_name_d295a09d": { - "message": "Duplicate name" - }, - "duplicate_skill_manifest_url_74d85839": { - "message": "Duplicate skill manifest Url" - }, - "duplicate_skill_name_d9f6eb8d": { - "message": "Duplicate skill name" - }, - "duplicated_intents_recognized_d3908424": { - "message": "Duplicated intents recognized" - }, - "early_adopters_e8db7999": { - "message": "Early adopters" - }, - "edit_a_publish_profile_30ebab3e": { - "message": "Edit a publish profile" - }, - "edit_a_skill_5665d9ac": { - "message": "Edit a skill" - }, - "edit_actions_b38e9fac": { - "message": "Edit Actions" - }, - "edit_an_array_property_5d886011": { - "message": "Edit an array property" - }, - "edit_array_4ab37c8": { - "message": "Edit Array" - }, - "edit_c5fbea07": { - "message": "Edit" - }, - "edit_displayname_dialog_description_986a7d60": { - "message": "Edit { displayName } dialog description" - }, - "edit_in_json_75d0d754": { - "message": "Edit in JSON" - }, - "edit_mode_7e07cfdf": { - "message": "Edit mode" - }, - "edit_property_dd6a1172": { - "message": "Edit Property" - }, - "ejecting_runtime_f6c90614": { - "message": "Ejecting runtime..." - }, - "emit_a_custom_event_78cf318b": { - "message": "Emit a custom event" - }, - "emit_a_telemetry_track_event_e2442842": { - "message": "Emit a telemetry track event" - }, - "emit_a_trace_event_f653ae84": { - "message": "Emit a trace event" - }, - "emit_event_32aa6583": { - "message": "Emit Event" - }, - "enable_6f5d1328": { - "message": "Enable" - }, - "enable_line_numbers_to_refer_to_code_lines_by_numb_e5ba66ea": { - "message": "Enable line numbers to refer to code lines by number." - }, - "enabled_ba7cab66": { - "message": "Enabled" - }, - "end_dialog_8f562a4c": { - "message": "End Dialog" - }, - "end_this_dialog_3ed0d50b": { - "message": "End this dialog" - }, - "end_turn_6ab71cea": { - "message": "End Turn" - }, - "end_turn_ca85b3d4": { - "message": "End turn" - }, - "endofconversation_activity_4aa21306": { - "message": "EndOfConversation activity" - }, - "english_us_1d884ccd": { - "message": "English (US)" - }, - "enter_a_manifest_url_to_add_a_new_skill_to_your_bo_57e9d660": { - "message": "Enter a manifest url to add a new skill to your bot." - }, - "enter_a_question_74f2e045": { - "message": "Enter a question" - }, - "enter_an_answer_84152a83": { - "message": "Enter an answer" - }, - "environment_68aed6d3": { - "message": "Environment" - }, - "equals_expression_47199f4e": { - "message": "Equals Expression" - }, - "error_98e81528": { - "message": "Error" - }, - "error_afac7133": { - "message": "Error:" - }, - "error_event_c079b608": { - "message": "Error event" - }, - "error_func_does_not_have_an_evaluator_it_s_not_a_b_8c660980": { - "message": "Error: { func } does not have an evaluator, it''s not a built-in function or a custom function" - }, - "error_in_ui_schema_for_title_errormsg_options_7f3c22f2": { - "message": "Error in UI schema for { title }: { errorMsg }\n{ options }" - }, - "error_occured_ejecting_runtime_76bb48e7": { - "message": "Error occured ejecting runtime!" - }, - "error_occurred_5549a6b4": { - "message": "Error occurred" - }, - "error_processing_schema_2c707cf3": { - "message": "Error Processing Schema" - }, - "event_activity_2067a94b": { - "message": "Event activity" - }, - "event_created_e9e05afc": { - "message": "Event created" - }, - "event_focused_51e447f3": { - "message": "Event focused" - }, - "event_received_457f99d6": { - "message": "Event received" - }, - "events_cf7a8c50": { - "message": "Events" - }, - "example_bot_list_9be1d563": { - "message": "Example bot list" - }, - "examples_c435f08c": { - "message": "Examples" - }, - "existing_files_in_scripts_folder_will_be_overwritt_afa8d787": { - "message": "Existing files in scripts/folder will be overwritten. Are you sure you want to continue?" - }, - "expand_2f2fadbd": { - "message": "Expand" - }, - "expand_navigation_20330d1d": { - "message": "Expand Navigation" - }, - "expected_responses_intent_intentname_44b051c": { - "message": "Expected responses (intent: #{ intentName })" - }, - "export_as_skill_764cf284": { - "message": "Export as skill" - }, - "export_assets_to_zip_d2d798ab": { - "message": "Export assets to .zip" - }, - "export_de71cd8e": { - "message": "Export" - }, - "expression_7f906a13": { - "message": "Expression" - }, - "expression_starting_with_a750efc8": { - "message": "Expression starting with =." - }, - "expression_to_evaluate_ce4095b1": { - "message": "Expression to evaluate." - }, - "extension_name_e21a5e25": { - "message": "Extension name" - }, - "extensions_7250a0bb": { - "message": "Extensions" - }, - "external_resources_will_not_be_changed_c08b0009": { - "message": "External resources will not be changed." - }, - "extract_question_and_answer_pairs_from_an_online_f_7316548e": { - "message": "Extract question-and-answer pairs from an online FAQ, product manuals, or other files. Supported formats are .tsv, .pdf, .doc, .docx, .xlsx, containing questions and answers in sequence. Learn more about knowledge base sources. Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. Learn more about QnA Maker SKUs." - }, - "extract_question_and_answer_pairs_from_an_online_f_c1e12724": { - "message": "Extract question-and-answer pairs from an online FAQ, product manuals, or other files. Supported formats are .tsv, .pdf, .doc, .docx, .xlsx, containing questions and answers in sequence. " - }, - "extracting_qna_pairs_from_urls_eaed1b1c": { - "message": "Extracting QNA pairs from { urls }" - }, - "false_2f39ee6d": { - "message": "false" - }, - "false_eef8c169": { - "message": "False" - }, - "field_set_6e7d7f67": { - "message": "Field Set" - }, - "file_name_8fd421ff": { - "message": "File name" - }, - "file_not_found_9eec054c": { - "message": "File not found" - }, - "file_or_attachment_5467e604": { - "message": "File or attachment" - }, - "file_type_fd1ba7ee": { - "message": "File Type" - }, - "filter_dialog_de3ce43f": { - "message": "Filter Dialog" - }, - "firstselector_a3daca5d": { - "message": "FirstSelector" - }, - "folder_foldername_already_exists_4a2260e9": { - "message": "folder { folderName } already exists" - }, - "for_each_def04c48": { - "message": "For Each" - }, - "for_each_page_3b4d4b69": { - "message": "For Each Page" - }, - "form_editor_7c2b02f0": { - "message": "form editor" - }, - "form_title_baf85c7e": { - "message": "form title" - }, - "fromtemplatename_does_not_exist_d429483c": { - "message": "fromTemplateName does not exist" - }, - "gb_7570760e": { - "message": "GB" - }, - "general_24ac26a8": { - "message": "General" - }, - "generate_44e33e72": { - "message": "Generate" - }, - "get_a_new_copy_of_the_runtime_code_84970bf": { - "message": "Get a new copy of the runtime code" - }, - "get_started_50c13c6c": { - "message": "Get started!" - }, - "getting_help_ab6811b0": { - "message": "Getting Help" - }, - "getting_started_f45a7e87": { - "message": "Getting Started" - }, - "go_to_qna_all_up_view_page_d475333d": { - "message": "Go to QnA all-up view page." - }, - "got_it_2c06b54a": { - "message": "Got it!" - }, - "greeting_f906f962": { - "message": "Greeting" - }, - "handle_a_condition_f32eb8d": { - "message": "Handle a Condition" - }, - "handoff_activity_14363a20": { - "message": "Handoff activity" - }, - "handover_to_human_1a619574": { - "message": "Handover to human" - }, - "here_s_what_we_know_4e9c1731": { - "message": "Here’s what we know…" - }, - "hide_code_5dcffa94": { - "message": "Hide code" - }, - "home_351838cd": { - "message": "Home" - }, - "http_request_79847109": { - "message": "HTTP Request" - }, - "i_want_to_delete_this_bot_f81a4735": { - "message": "I want to delete this bot" - }, - "icon_name_is_file_c80dacae": { - "message": "{ icon } name is { file }" - }, - "iconname_file_icon_29976c8e": { - "message": "{ iconName } file icon" - }, - "id_already_exists_please_enter_a_unique_file_name_174913a3": { - "message": "{ id } already exists. Please enter a unique file name." - }, - "id_is_id_c0fc7a7d": { - "message": "id is { id }" - }, - "if_condition_56c9be4a": { - "message": "If Condition" - }, - "if_this_problem_persists_please_file_an_issue_on_6fbc8e2b": { - "message": "If this problem persists, please file an issue on" - }, - "if_you_already_have_a_luis_account_provide_the_inf_bede07a4": { - "message": "If you already have a LUIS account, provide the information below. If you do not have an account yet, create a (free) account first." - }, - "if_you_already_have_a_qna_account_provide_the_info_466d6a4b": { - "message": "If you already have a QNA account, provide the information below. If you do not have an account yet, create a (free) account first." - }, - "import_qna_from_url_26a99161": { - "message": "Import QnA From Url" - }, - "in_production_5a70b8b4": { - "message": "In production" - }, - "in_test_63c32694": { - "message": "In test" - }, - "in_the_b_create_a_trigger_b_wizard_set_the_trigger_f9b23519": { - "message": "In the Create a trigger wizard, set the trigger type to Activities in the dropdown. Then set the Activity Type to Greeting (ConversationUpdate activity), and click the Submit button." - }, - "increment_by_step_1cf3a88": { - "message": "increment by { step }" - }, - "input_1d1d9b8e": { - "message": "Input" - }, - "install_microsoft_net_core_sdk_2de509f0": { - "message": "Install Microsoft .NET Core SDK" - }, - "install_pre_release_versions_of_composer_daily_to__ceb41b54": { - "message": "Install pre-release versions of Composer, daily, to access and test the latest features. Learn more." - }, - "install_the_update_and_restart_composer_fac30a61": { - "message": "Install the update and restart Composer." - }, - "integer_b08abbe9": { - "message": "Integer" - }, - "integer_constant_650191ba": { - "message": "Integer constant." - }, - "integer_constant_or_expression_to_evaluate_ec4a17da": { - "message": "Integer constant or expression to evaluate." - }, - "integer_or_expression_107f60fb": { - "message": "Integer or expression" - }, - "intent_2291200b": { - "message": "Intent" - }, - "intent_recognized_c3840853": { - "message": "Intent recognized" - }, - "intentname_is_missing_or_empty_e49db2f8": { - "message": "intentName is missing or empty" - }, - "interpolated_string_c96053f2": { - "message": "Interpolated string." - }, - "interpolated_string_e04923d3": { - "message": "Interpolated string" - }, - "interpolated_string_or_expression_to_evaluate_ba0b0ba": { - "message": "Interpolated string or expression to evaluate." - }, - "introduction_of_key_concepts_and_user_experience_e_971b32e9": { - "message": "Introduction of key concepts and user experience elements for Composer." - }, - "invoke_activity_87df4903": { - "message": "Invoke activity" - }, - "is_missing_or_empty_a551462e": { - "message": "is missing or empty" - }, - "it_s_not_a_built_in_function_or_a_custom_function_1527ed83": { - "message": "it''s not a built-in function or a custom function." - }, - "item_actions_22d0242": { - "message": "Item Actions" - }, - "item_actions_cd903bde": { - "message": "Item actions" - }, - "item_added_f3910bed": { - "message": "item added" - }, - "jan_28_2020_8beb36dc": { - "message": "Jan 28, 2020" - }, - "kb_d9c53902": { - "message": "KB" - }, - "key_cannot_be_blank_dccc1b46": { - "message": "Key cannot be blank" - }, - "key_f2a919d5": { - "message": "Key" - }, - "keys_must_be_unique_2028cef3": { - "message": "Keys must be unique" - }, - "l_startline_startcharacter_l_endline_endcharacter_72bc2e5d": { - "message": "L{ startLine }:{ startCharacter } - L{ endLine }:{ endCharacter } " - }, - "language_generation_1876f6d6": { - "message": "Language Generation" - }, - "language_generation_file_80c08efe": { - "message": "language generation file" - }, - "language_understanding_9ae3f1f6": { - "message": "Language Understanding" - }, - "language_understanding_file_a65abc2c": { - "message": "language understanding file" - }, - "languagepolicy_e754ad28": { - "message": "LanguagePolicy" - }, - "last_modified_time_is_time_b1497d3": { - "message": "Last modified time is { time }" - }, - "learn_more_a79a7918": { - "message": "Learn more" - }, - "learn_more_about_knowledge_base_sources_24369b09": { - "message": "Learn more about knowledge base sources. " - }, - "learn_more_about_qna_maker_skus_998c567": { - "message": "Learn more about QnA Maker SKUs." - }, - "learn_more_about_title_d1d3edbe": { - "message": "Learn more about { title }" - }, - "learn_more_c08939e8": { - "message": "Learn more." - }, - "learn_the_basics_2d9ae7df": { - "message": "Learn the basics" - }, - "leave_product_tour_49585718": { - "message": "Leave Product Tour?" - }, - "lg_editor_ee0184e6": { - "message": "LG editor" - }, - "lg_file_already_exist_55195d20": { - "message": "lg file already exist" - }, - "lg_file_id_not_found_6bd6869b": { - "message": "LG file { id } not found" - }, - "lg_language_client_7892c4c1": { - "message": "LG Language Client" - }, - "lg_navigation_pane_97b15c3d": { - "message": "LG Navigation Pane" - }, - "line_numbers_1bfa7fb": { - "message": "Line numbers" - }, - "line_startline_startcharacter_line_endline_endchar_372bb2f4": { - "message": "line { startLine }:{ startCharacter } - line { endLine }:{ endCharacter }" - }, - "link_to_where_this_luis_intent_is_defined_9be25fb7": { - "message": "link to where this LUIS intent is defined" - }, - "list_view_e33843f0": { - "message": "List view" - }, - "loading_25990131": { - "message": "Loading..." - }, - "loading_bde52856": { - "message": "Loading" - }, - "location_e17bd402": { - "message": "Location" - }, - "location_is_location_8c17b5de": { - "message": "location is { location }" - }, - "log_to_console_4fc23e34": { - "message": "Log to console" - }, - "login_6f3d6249": { - "message": "Login" - }, - "login_to_composer_eb42b41": { - "message": "Login to Composer" - }, - "loop_for_each_item_53eb7c5b": { - "message": "Loop: for each item" - }, - "loop_for_each_item_e09537ae": { - "message": "Loop: For each item" - }, - "loop_for_each_page_multiple_items_733e7f41": { - "message": "Loop: For each page (multiple items)" - }, - "looping_ddae56ff": { - "message": "Looping" - }, - "lu_editor_d09fb2b0": { - "message": "LU editor" - }, - "lu_file_id_not_found_8732d33e": { - "message": "LU file { id } not found" - }, - "lu_language_client_bbffcd7b": { - "message": "LU Language Client" - }, - "lu_navigation_pane_54dc5587": { - "message": "LU Navigation Pane" - }, - "luis_add4bbe3": { - "message": "LUIS" - }, - "luis_authoring_key_cfaba7dd": { - "message": "LUIS Authoring key:" - }, - "luis_authoring_region_b142f97b": { - "message": "Luis Authoring Region" - }, - "main_dialog_eed5c847": { - "message": "Main dialog" - }, - "make_a_copy_77d1233": { - "message": "Make a copy" - }, - "manage_properties_c9fa4456": { - "message": "Manage properties" - }, - "manifest_url_30824e88": { - "message": "Manifest url" - }, - "manifest_url_can_not_be_accessed_a7f147b2": { - "message": "Manifest url can not be accessed" - }, - "manifest_version_1edc004a": { - "message": "Manifest Version" - }, - "mb_8f9f9e84": { - "message": "MB" - }, - "message_5c38209d": { - "message": "Message" - }, - "message_deleted_628eef3a": { - "message": "Message deleted" - }, - "message_deleted_activity_85dd8915": { - "message": "Message deleted activity" - }, - "message_reaction_3704d790": { - "message": "Message reaction" - }, - "message_reaction_activity_379944e7": { - "message": "Message reaction activity" - }, - "message_received_5abfe9a0": { - "message": "Message received" - }, - "message_received_activity_1ff7c2a4": { - "message": "Message received activity" - }, - "message_updated_4f2e37fe": { - "message": "Message updated" - }, - "message_updated_activity_4997737e": { - "message": "Message updated activity" - }, - "microsoft_app_id_a7f3e591": { - "message": "Microsoft App Id" - }, - "microsoft_app_password_737ebc90": { - "message": "Microsoft App Password" - }, - "minimap_beb3be27": { - "message": "Minimap" - }, - "missing_definition_for_defname_33f2b594": { - "message": "Missing definition for { defName }" - }, - "missing_fields_1c88ab71": { - "message": "missing fields" - }, - "modification_rejected_6a6e8322": { - "message": "Modification Rejected" - }, - "modify_active_dialog_edcf4a45": { - "message": "Modify active dialog" - }, - "modify_this_dialog_3c38af24": { - "message": "Modify this dialog" - }, - "more_options_e89670cf": { - "message": "More options" - }, - "mostspecificselector_2cf1a6ae": { - "message": "MostSpecificSelector" - }, - "move_abf00365": { - "message": "Move" - }, - "move_down_eaae3426": { - "message": "Move Down" - }, - "move_up_b1c4d3a5": { - "message": "Move Up" - }, - "msft_ignite_ai_show_e131edef": { - "message": "MSFT Ignite AI Show" - }, - "multi_choice_839b54bb": { - "message": "Multi-choice" - }, - "multiple_wildcards_26f50b6c": { - "message": "multiple wildcards" - }, - "must_be_an_expression_error_477cbda6": { - "message": "must be an expression: { error }" - }, - "must_have_a_name_d5c5c464": { - "message": "Must have a name" - }, - "my_publish_profile_2ed56828": { - "message": "My Publish Profile" - }, - "name_1aed4a1b": { - "message": "Name" - }, - "name_and_save_your_skill_manifest_cfd672b7": { - "message": "Name and save your skill manifest." - }, - "name_cannot_include_special_characters_or_spaces_59a1950b": { - "message": "Name cannot include special characters or spaces" - }, - "name_copy_55d27c1a": { - "message": "{ name }_Copy" - }, - "name_is_name_abc564f9": { - "message": "Name is { name }" - }, - "name_is_required_29d28f8d": { - "message": "{ name } is required" - }, - "name_of_skill_dialog_to_call_201707f3": { - "message": "Name of skill dialog to call" - }, - "navigation_control_94f2649e": { - "message": "navigation control" - }, - "navigation_pane_e587b73c": { - "message": "Navigation pane" - }, - "navigation_panel_530ae4ac": { - "message": "Navigation panel" - }, - "navigation_path_8b299e64": { - "message": "Navigation Path" - }, - "navigation_to_see_actions_3be545c9": { - "message": "navigation to see actions" - }, - "new_13daf639": { - "message": "New" - }, - "new_template_49e6f0f2": { - "message": "New template" - }, - "new_trigger_331c811b": { - "message": "New Trigger .." - }, - "new_update_available_30534ea5": { - "message": "New update available" - }, - "new_value_3c1ce474": { - "message": "New value" - }, - "newsectionplaceholdersectionname_5fc0d21": { - "message": "_NewSectionPlaceHolderSectionName" - }, - "next_40e12421": { - "message": "Next" - }, - "no_editor_for_type_8b5593c5": { - "message": "No Editor for { type }" - }, - "no_lu_file_with_name_id_fb21315d": { - "message": "NO LU FILE WITH NAME { id }" - }, - "no_lu_or_qna_file_with_name_id_21cfe9dc": { - "message": "NO LU OR QNA FILE WITH NAME { id }" - }, - "no_updates_available_cecd904d": { - "message": "No updates available" - }, - "no_wildcard_ff439e76": { - "message": "no wildcard" - }, - "node_menu_e2aa8092": { - "message": "Node menu" - }, - "not_a_single_template_e37f894": { - "message": "Not a single template" - }, - "not_yet_published_669e37b3": { - "message": "Not yet published" - }, - "notification_list_ee2abb6c": { - "message": "Notification list" - }, - "notification_message_msg_7d55d08e": { - "message": "Notification Message { msg }" - }, - "notification_type_263c145d": { - "message": "Notification type" - }, - "notifications_cbfa7704": { - "message": "Notifications" - }, - "nov_12_2019_96ec5473": { - "message": "Nov 12, 2019" - }, - "number_a6dc44e": { - "message": "Number" - }, - "number_constant_1938e96e": { - "message": "Number constant." - }, - "number_constant_or_expression_to_evaluate_1098771": { - "message": "Number constant or expression to evaluate." - }, - "number_or_expression_55c7f9f": { - "message": "Number or expression" - }, - "numeric_field_c2564f69": { - "message": "numeric field" - }, - "oauth_login_b6aa9534": { - "message": "OAuth login" - }, - "object_345070f6": { - "message": "Object" - }, - "object_constant_77052b91": { - "message": "Object constant." - }, - "object_or_expression_89903fa1": { - "message": "Object or expression" - }, - "object_or_expression_to_evaluate_699c8827": { - "message": "Object or expression to evaluate." - }, - "off_5385ef3f": { - "message": "Off" - }, - "ok_c47544a2": { - "message": "OK" - }, - "ok_ff1b646a": { - "message": "Ok" - }, - "okay_1772bbeb": { - "message": "Okay" - }, - "on_8f7190e5": { - "message": "On" - }, - "onboarding_8407871c": { - "message": "Onboarding" - }, - "ondialogevents_types_3dc569b5": { - "message": "OnDialogEvents Types" - }, - "open_e0beb7b9": { - "message": "Open" - }, - "open_inline_editor_a5aabcfa": { - "message": "Open inline editor" - }, - "or_4f7d4edb": { - "message": "Or: " - }, - "origin_lg_file_not_found_in_store_d194cdbc": { - "message": "origin lg file not found in store" - }, - "origin_lu_file_not_found_in_store_19e5cc8f": { - "message": "origin lu file not found in store" - }, - "original_ce7b7e64": { - "message": " - Original" - }, - "other_1c6d9c79": { - "message": "Other" - }, - "output_5023cf84": { - "message": "Output" - }, - "page_number_cdee4179": { - "message": "Page number" - }, - "parse_failed_at_name_error_8f562bda": { - "message": "Parse failed at { name }: { error }" - }, - "paste_5963d1c1": { - "message": "Paste" - }, - "please_add_at_least_minitems_endpoint_5439fd74": { - "message": "Please add at least { minItems } endpoint" - }, - "please_enter_a_value_for_key_77cfc097": { - "message": "Please enter a value for { key }" - }, - "please_enter_an_event_name_a148275a": { - "message": "Please enter an event name" - }, - "please_input_a_manifest_url_d726edbf": { - "message": "Please input a manifest Url" - }, - "please_input_regex_pattern_5cd659a2": { - "message": "Please input regEx pattern" - }, - "please_log_in_before_continuing_b6eace13": { - "message": "Please log in before continuing." - }, - "please_return_to_design_view_to_start_the_onboardi_a561af9d": { - "message": "Please return to Design View to start the Onboarding tutorial." - }, - "please_select_a_event_type_200c4c9f": { - "message": "Please select a event type" - }, - "please_select_a_specific_qna_file_to_import_qna_d71d871": { - "message": "Please select a specific qna file to import QnA" - }, - "please_select_a_trigger_type_67417abb": { - "message": "Please select a trigger type" - }, - "please_select_a_valid_endpoint_bf608af1": { - "message": "Please select a valid endpoint" - }, - "please_select_a_version_of_the_manifest_schema_4a3efbb1": { - "message": "Please select a version of the manifest schema" - }, - "please_select_an_activity_type_92f4a8a1": { - "message": "Please select an activity type" - }, - "populate_your_knowledge_base_bb2d3605": { - "message": "Populate your Knowledge Base" - }, - "press_enter_to_add_this_item_or_tab_to_move_to_the_6beb8a14": { - "message": "press Enter to add this item or Tab to move to the next interactive element" - }, - "press_enter_to_add_this_name_and_advance_to_the_ne_6a2ae080": { - "message": "press Enter to add this name and advance to the next row, or press Tab to advance to the value field" - }, - "previous_bd2ac015": { - "message": "Previous" - }, - "previous_folder_e7eeb306": { - "message": "previous folder" - }, - "privacy_290109ea": { - "message": "Privacy" - }, - "privacy_button_b58e437": { - "message": "Privacy button" - }, - "progress_of_total_87de8616": { - "message": "{ progress }% of { total }" - }, - "prompt_for_a_date_5d2c689e": { - "message": "Prompt for a date" - }, - "prompt_for_a_date_or_a_time_d2df7f90": { - "message": "Prompt for a date or a time" - }, - "prompt_for_a_file_or_an_attachment_1bf18e7e": { - "message": "Prompt for a file or an attachment" - }, - "prompt_for_a_number_84999edb": { - "message": "Prompt for a number" - }, - "prompt_for_attachment_727d4fac": { - "message": "Prompt for Attachment" - }, - "prompt_for_confirmation_dc85565c": { - "message": "Prompt for confirmation" - }, - "prompt_for_text_5c524f80": { - "message": "Prompt for text" - }, - "prompt_with_multi_choice_f428542f": { - "message": "Prompt with multi-choice" - }, - "property_description_8d21ea2e": { - "message": "Property description." - }, - "property_editor_preferences_ad7a4b62": { - "message": "Property editor preferences" - }, - "property_title_f2b443b7": { - "message": "Property title" - }, - "property_type_95689fa5": { - "message": "Property type." - }, - "provide_a_brief_description_it_will_appear_on_the__f962eb38": { - "message": "Provide a brief description. It will appear on the publish history list." - }, - "pseudo_1a319287": { - "message": "Pseudo" - }, - "publish_5211dca3": { - "message": "Publish" - }, - "publish_configuration_d759a4e3": { - "message": "Publish Configuration" - }, - "publish_destination_type_5f8c200a": { - "message": "Publish Destination Type" - }, - "publish_models_9a36752a": { - "message": "Publish models" - }, - "publish_profiles_cf8d307b": { - "message": "Publish Profiles" - }, - "publish_to_selected_profile_c0998a2": { - "message": "Publish to selected profile" - }, - "published_4bb5209e": { - "message": "Published" - }, - "publishing_d63a8f2d": { - "message": "Publishing" - }, - "qna_28ee5e26": { - "message": "QnA" - }, - "qna_editor_9eb94b02": { - "message": "QnA editor" - }, - "qna_file_37705cb7": { - "message": "qna file" - }, - "qna_intent_recognized_49c3d797": { - "message": "QnA Intent recognized" - }, - "qna_navigation_pane_b79ebcbf": { - "message": "Qna Navigation Pane" - }, - "qna_region_5a864ef8": { - "message": "QnA Region" - }, - "qna_subscription_key_ed72a47": { - "message": "QNA Subscription key:" - }, - "question_9121487": { - "message": "Question" - }, - "randomselector_4a5274f1": { - "message": "RandomSelector" - }, - "range_selection_30caeea5": { - "message": "Range Selection" - }, - "re_prompt_for_input_c3b5b3ab": { - "message": "Re-prompt for input" - }, - "recent_bots_53585911": { - "message": "Recent Bots" - }, - "recognizer_type_dc591e16": { - "message": "Recognizer Type" - }, - "recognizers_cefce9d1": { - "message": "Recognizers" - }, - "redo_363c58b7": { - "message": "Redo" - }, - "redo_is_not_supported_b743e4dc": { - "message": "Redo is not supported" - }, - "refer_to_the_syntax_documentation_here_df8dc9b4": { - "message": "Refer to the syntax documentation here." - }, - "regex_intent_is_already_defined_df095c1f": { - "message": "RegEx { intent } is already defined" - }, - "regular_expression_855557bf": { - "message": "Regular Expression" - }, - "release_1af20f26": { - "message": "Release: " - }, - "reloading_49d2f661": { - "message": "Reloading" - }, - "remove_f47dc62a": { - "message": "Remove" - }, - "repeat_this_dialog_83ca994e": { - "message": "Repeat this dialog" - }, - "replace_this_dialog_e304015e": { - "message": "Replace this dialog" - }, - "reprompt_dialog_event_c42d2c33": { - "message": "Reprompt dialog event" - }, - "response_is_response_3cd62f8f": { - "message": "Response is { response }" - }, - "responses_12d6df1d": { - "message": "Responses" - }, - "restart_bot_34e36428": { - "message": "Restart Bot" - }, - "review_and_generate_63dec712": { - "message": "Review and generate" - }, - "rollback_26326307": { - "message": "Rollback" - }, - "runtime_already_exists_f181f5ec": { - "message": "Runtime already exists" - }, - "runtime_code_location_4e94ee12": { - "message": "Runtime code location" - }, - "runtime_config_a2904ff9": { - "message": "Runtime Config" - }, - "sample_phrases_5d78fa35": { - "message": "Sample Phrases" - }, - "sample_phrases_are_phrases_a966b47f": { - "message": "Sample Phrases are { phrases }" - }, - "save_11a80ec3": { - "message": "Save" - }, - "save_as_9e0cf70b": { - "message": "Save as" - }, - "save_your_skill_manifest_63bf5f26": { - "message": "Save your skill manifest" - }, - "schema_24739a48": { - "message": "Schema" - }, - "scripts_successfully_updated_3a75d57f": { - "message": "Scripts successfully updated." - }, - "sdk_runtime_packages_22d8ed1a": { - "message": "SDK runtime packages" - }, - "search_280d00bd": { - "message": "Search" - }, - "search_for_extensions_a27c4772": { - "message": "Search for extensions" - }, - "see_log_4b833bf7": { - "message": "See Log" - }, - "select_a_bot_e1c4dc2b": { - "message": "Select a Bot" - }, - "select_a_trigger_on_the_left_a4b41558": { - "message": "Select a trigger on the left" - }, - "select_all_f73344a8": { - "message": "Select all" - }, - "select_an_activity_type_c982fcbe": { - "message": "Select an activity type" - }, - "select_an_event_type_3d7108f1": { - "message": "Select an event type" - }, - "select_language_to_delete_d1662d3d": { - "message": "Select language to delete" - }, - "select_manifest_version_4f5b1230": { - "message": "Select manifest version" - }, - "select_options_9ee7b227": { - "message": "Select options" - }, - "select_property_type_5f3ab685": { - "message": "select property type" - }, - "select_runtime_version_to_add_d63d383b": { - "message": "Select runtime version to add" - }, - "select_the_language_that_bot_will_be_able_to_under_1f2bcb96": { - "message": "Select the language that bot will be able to understand (User input) and respond to (Bot responses).\n To make this bot available in other languages, click “Add’ to create a copy of the default language, and translate the content into the new language." - }, - "select_which_dialogs_are_included_in_the_skill_man_281ef8c9": { - "message": "Select which dialogs are included in the skill manifest" - }, - "select_which_tasks_this_skill_can_perform_172b0eae": { - "message": "Select which tasks this skill can perform" - }, - "selection_field_86d1dc94": { - "message": "selection field" - }, - "selectors_2dcb3029": { - "message": "Selectors" - }, - "send_a_response_1a917d7e": { - "message": "Send a response" - }, - "send_an_http_request_aa32fd2": { - "message": "Send an HTTP request" - }, - "send_messages_c48b239": { - "message": "Send Messages" - }, - "sentence_wrap_930c8ced": { - "message": "Sentence wrap" - }, - "session_expired_12aaf414": { - "message": "Session expired" - }, - "set_a_property_4258d8d7": { - "message": "Set a property" - }, - "set_destination_folder_f76e0259": { - "message": "Set destination folder" - }, - "set_properties_7415af3c": { - "message": "Set properties" - }, - "set_up_your_bot_75009578": { - "message": "Set up your bot" - }, - "settings_5aa0fd0c": { - "message": "Settings" - }, - "settings_contains_detailed_information_about_your__54aa601d": { - "message": "Settings contains detailed information about your bot. For security reasons, they are hidden by default. To test your bot or publish to Azure, you may need to provide these settings." - }, - "settings_editor_b5246933": { - "message": "Settings editor" - }, - "settings_menu_c99ecc6d": { - "message": "Settings menu" - }, - "show_all_notifications_55bf7858": { - "message": "Show All Notifications" - }, - "show_code_f3e9d1cc": { - "message": "Show code" - }, - "show_keys_3072a5b8": { - "message": "Show keys" - }, - "show_skill_manifest_5d0abde1": { - "message": "Show skill manifest" - }, - "sign_out_user_6845d640": { - "message": "Sign out user" - }, - "skill_dialog_name_1bbf0eff": { - "message": "Skill Dialog Name" - }, - "skill_endpoint_b563491e": { - "message": "Skill Endpoint" - }, - "skill_endpoints_e4e3d8c1": { - "message": "Skill endpoints" - }, - "skill_host_endpoint_b1088d0": { - "message": "Skill Host Endpoint" - }, - "skill_manifest_endpoint_is_configured_improperly_e083731d": { - "message": "Skill manifest endpoint is configured improperly" - }, - "skillname_manifest_ef3d9fed": { - "message": "{ skillName } Manifest" - }, - "skills_49cccd6a": { - "message": "Skills" - }, - "skip_this_step_to_add_questions_and_answers_manual_ed1b9f80": { - "message": "Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. " - }, - "sorry_something_went_wrong_with_connecting_bot_run_7d6785e3": { - "message": "Sorry, something went wrong with connecting bot runtime" - }, - "sorry_something_went_wrong_with_publishing_try_aga_306a48f5": { - "message": "Sorry, something went wrong with publishing. Try again or exit out of this task." - }, - "sorted_a_to_z_915b2ed3": { - "message": "Sorted A to Z" - }, - "sorted_z_to_a_722f1567": { - "message": "Sorted Z to A" - }, - "spaces_and_special_characters_are_not_allowed_20d47684": { - "message": "Spaces and special characters are not allowed." - }, - "spaces_and_special_characters_are_not_allowed_use__48acec3c": { - "message": "Spaces and special characters are not allowed. Use letters, numbers, -, or _." - }, - "specify_a_name_and_description_for_your_new_dialog_86eb3130": { - "message": "Specify a name and description for your new dialog." - }, - "specify_a_name_description_and_location_for_your_n_667f1438": { - "message": "Specify a name, description, and location for your new bot project." - }, - "start_bot_25ecad14": { - "message": "Start Bot" - }, - "start_bot_failed_d75647d5": { - "message": "Start bot failed" - }, - "start_command_a085f2ec": { - "message": "Start command" - }, - "state_is_state_a2b8943": { - "message": "State is { state }" - }, - "status_e7fdbe06": { - "message": "Status" - }, - "step_of_setlength_43c73821": { - "message": "{ step } of { setLength }" - }, - "string_24369b3": { - "message": "String" - }, - "string_field_db491570": { - "message": "string field" - }, - "string_or_expression_c441b45c": { - "message": "String or expression" - }, - "submit_a3cc6859": { - "message": "Submit" - }, - "synonyms_optional_afe5cdb1": { - "message": "Synonyms (Optional)" - }, - "tb_149f379c": { - "message": "TB" - }, - "templatename_is_missing_or_empty_23e6b06e": { - "message": "templateName is missing or empty" - }, - "terms_of_use_6542769b": { - "message": "Terms of Use" - }, - "test_in_emulator_b1b3c278": { - "message": "Test in Emulator" - }, - "test_your_bot_3cd1f4bb": { - "message": "Test your bot" - }, - "text_7f4593da": { - "message": "Text" - }, - "the_callback_url_for_the_skill_host_e20e1012": { - "message": "The callback url for the skill host." - }, - "the_dialog_you_have_tried_to_delete_is_currently_u_a37c7a02": { - "message": "The dialog you have tried to delete is currently used in the below dialog(s). Removing this dialog will cause your Bot to malfunction without additional action." - }, - "the_following_lufile_s_are_invalid_c61ea748": { - "message": "The Following LuFile(s) are invalid: \n" - }, - "the_main_dialog_is_named_after_your_bot_it_is_the__3d9864f": { - "message": "The main dialog is named after your bot. It is the root and entry point of a bot." - }, - "the_manifest_can_be_edited_and_refined_manually_if_9269e3f2": { - "message": "The manifest can be edited and refined manually if and where needed." - }, - "the_microsoft_app_id_that_will_be_calling_the_skil_955424de": { - "message": "The Microsoft App Id that will be calling the skill." - }, - "the_microsoft_app_password_that_will_be_calling_th_acf9e1ea": { - "message": "The Microsoft App Password that will be calling the skill." - }, - "the_page_you_are_looking_for_can_t_be_found_a3b75795": { - "message": "The page you are looking for can''t be found." - }, - "the_return_type_does_not_match_2ae72548": { - "message": "the return type does not match" - }, - "the_welcome_message_is_triggered_by_the_i_conversa_a3ff58f8": { - "message": "The welcome message is triggered by the ConversationUpdate event. To add a new ConversationUpdate trigger:" - }, - "there_was_an_error_74ed3c58": { - "message": "There was an error" - }, - "there_was_error_creating_your_kb_53b31ff3": { - "message": "There was error creating your KB" - }, - "these_examples_bring_together_all_of_the_best_prac_ca1b89c7": { - "message": "These examples bring together all of the best practices and supporting components we''ve identified through building of conversational experiences." - }, - "these_tasks_will_be_used_to_generate_the_manifest__2791be0e": { - "message": "These tasks will be used to generate the manifest and describe the capabilities of this skill to those who may want to use it." - }, - "this_configures_a_data_driven_dialog_via_a_collect_c7fa4389": { - "message": "This configures a data driven dialog via a collection of events and actions." - }, - "this_dialog_has_no_trigger_yet_d1f1d173": { - "message": "This dialog has no trigger yet." - }, - "this_is_a_required_field_acb9837e": { - "message": "This is a required field." - }, - "this_is_a_severity_notification_9beabb58": { - "message": "This is a { severity } notification" - }, - "this_is_the_language_used_for_composer_s_user_inte_ab7fa82e": { - "message": "This is the language used for Composer’s user interface." - }, - "this_language_will_be_copied_and_used_as_the_basis_573515e4": { - "message": "This language will be copied and used as the basis (and fallback language) for the translation." - }, - "this_trigger_type_is_not_supported_by_the_regex_re_2c3b7c46": { - "message": "This trigger type is not supported by the RegEx recognizer and will not be fired." - }, - "this_trigger_type_is_not_supported_by_the_regex_re_dc3eefa2": { - "message": "This trigger type is not supported by the RegEx recognizer. To ensure this trigger is fired, change the recognizer type." - }, - "this_url_is_duplicated_a0768f44": { - "message": "This url is duplicated" - }, - "this_version_of_the_content_is_out_of_date_and_you_5e878f29": { - "message": "This version of the content is out of date, and your last change was rejected. The content will be automatically refreshed." - }, - "this_will_delete_the_dialog_and_its_contents_do_yo_9b48fa3c": { - "message": "This will delete the Dialog and its contents. Do you wish to continue?" - }, - "this_will_delete_the_profile_do_you_wish_to_contin_61b54894": { - "message": "This will delete the profile. Do you wish to continue?" - }, - "this_will_open_your_emulator_application_if_you_do_ba277151": { - "message": "This will open your Emulator application. If you don''t yet have the Bot Framework Emulator installed, you can download it here." - }, - "time_2b5aac58": { - "message": "Time" - }, - "tip_8f74cd0": { - "message": "tip" - }, - "tips_80d0da2b": { - "message": "tips" - }, - "title_ee03d132": { - "message": "Title" - }, - "title_msg_ee91458d": { - "message": "{ title }. { msg }" - }, - "to_customize_the_welcome_message_select_the_i_send_9b4bf4f": { - "message": "To customize the welcome message, select the Send a response action in the Visual Editor. Then in the Form Editor on the right, you can edit the bot''s welcome message in the Language Generation field." - }, - "to_learn_more_about_the_lg_file_format_read_the_do_ef6e083d": { - "message": "> To learn more about the LG file format, read the documentation at\n> { lgHelp }" - }, - "to_learn_more_about_the_lu_file_format_read_the_do_cac5ffc9": { - "message": "> To learn more about the LU file format, read the documentation at\n> { LU_HELP }" - }, - "to_learn_more_about_the_qna_file_format_read_the_d_1ce18259": { - "message": "> To learn more about the QnA file format, read the documentation at\n> { QNA_HELP }" - }, - "to_make_your_bot_available_for_others_as_a_skill_w_f2c19b9c": { - "message": "To make your bot available for others as a skill, we need to generate a manifest." - }, - "to_run_this_bot_composer_needs_net_core_sdk_d1551038": { - "message": "To run this bot, Composer needs .NET Core SDK." - }, - "to_understand_what_the_user_says_your_dialog_needs_4e791611": { - "message": "To understand what the user says, your dialog needs a \"Recognizer\"; that includes example words and sentences that users may use." - }, - "to_understand_what_the_user_says_your_dialog_needs_957034cc": { - "message": "To understand what the user says, your dialog needs an ''IRecognizer'' that includes example words and sentences that users may use." - }, - "to_which_language_will_you_be_translating_your_bot_77219d69": { - "message": "To which language will you be translating your bot?" - }, - "toolbar_bafd4228": { - "message": "toolbar" - }, - "total_mb_531a3721": { - "message": "{ total }MB" - }, - "trigger_phrases_f6754fa": { - "message": "Trigger phrases" - }, - "trigger_phrases_intent_intentname_a1b62148": { - "message": "Trigger phrases (intent: #{ intentName })" - }, - "triggers_connect_intents_with_bot_responses_think__fdfc97ea": { - "message": "Triggers connect intents with bot responses. Think of a trigger as one capability of your bot. So your bot is a collection of triggers. To add a new trigger, click the Add button in the toolbar, and then select the Add a new trigger option from the dropdown menu." - }, - "true_1900d7ae": { - "message": "true" - }, - "true_b9327890": { - "message": "True" - }, - "trueselector_40702dda": { - "message": "TrueSelector" - }, - "try_again_ad656c3c": { - "message": "Try again" - }, - "type_c8106334": { - "message": "Type" - }, - "type_could_not_be_loaded_65ebaf86": { - "message": "{ type } could not be loaded" - }, - "type_dialog_name_14a50769": { - "message": "Type dialog name" - }, - "typing_activity_6b634ae": { - "message": "Typing activity" - }, - "unable_to_determine_recognizer_type_from_data_valu_2960f526": { - "message": "Unable to determine recognizer type from data: { value }" - }, - "undo_a7be8fef": { - "message": "Undo" - }, - "undo_is_not_supported_ecd6f9fc": { - "message": "Undo is not supported" - }, - "unknown_intent_44b962ba": { - "message": "Unknown intent" - }, - "unknown_intent_recognized_1953f9be": { - "message": "Unknown intent recognized" - }, - "unknown_state_23f73afb": { - "message": "Unknown State" - }, - "unused_8d193e3": { - "message": "Unused" - }, - "update_a_an_activity_previously_sent_during_the_co_f0619cca": { - "message": "Update a an activity previously sent during the conversation" - }, - "update_activity_2b05e6c6": { - "message": "Update activity" - }, - "update_available_b637d767": { - "message": "Update available" - }, - "update_complete_c5163fbf": { - "message": "Update complete" - }, - "update_failed_2c87428c": { - "message": "Update failed" - }, - "update_folder_name_error_24563bf6": { - "message": "Update Folder Name Error" - }, - "update_in_progress_f65e6b29": { - "message": "Update in progress" - }, - "update_scripts_a3a483e": { - "message": "Update scripts" - }, - "update_scripts_c58771a2": { - "message": "Update Scripts" - }, - "updating_scripts_e17a5722": { - "message": "Updating scripts... " - }, - "url_22a5f3b8": { - "message": "URL" - }, - "url_8c4ff7d2": { - "message": "Url" - }, - "url_should_start_with_http_s_9ca55d94": { - "message": "Url should start with http[s]://" - }, - "use_custom_runtime_d7d323fd": { - "message": "Use custom runtime" - }, - "used_3d895705": { - "message": "Used" - }, - "used_in_126529e5": { - "message": "Used In" - }, - "user_input_673e4a89": { - "message": "User input" - }, - "user_input_a6ff658d": { - "message": "User Input" - }, - "user_is_typing_790cb502": { - "message": "User is typing" - }, - "validating_35b79a96": { - "message": "Validating..." - }, - "validation_rules_efd3144d": { - "message": "Validation Rules" - }, - "value_d842f16d": { - "message": "Value" - }, - "version_5599c321": { - "message": "Version" - }, - "version_version_a051e218": { - "message": "Version { version }" - }, - "video_tutorials_79eb26ca": { - "message": "Video tutorials:" - }, - "view_ba339f93": { - "message": "View" - }, - "view_kb_c382e495": { - "message": "View KB" - }, - "vishwac_sena_45910bf0": { - "message": "Vishwac Sena" - }, - "visual_editor_216472d": { - "message": "Visual editor" - }, - "warning_53c98b03": { - "message": "Warning!" - }, - "warning_the_action_you_are_about_to_take_cannot_be_1071a3c3": { - "message": "Warning: the action you are about to take cannot be undone. Going further will delete this bot and any related files in the bot project folder." - }, - "we_have_created_a_sample_bot_to_help_you_get_start_95a58922": { - "message": "We have created a sample bot to help you get started with Composer. Click here to open the bot." - }, - "we_need_to_define_the_endpoints_for_the_skill_to_a_5dc98d90": { - "message": "We need to define the endpoints for the skill to allow other bots to interact with it." - }, - "weather_bot_c38920cd": { - "message": "Weather Bot" - }, - "welcome_73d18b4d": { - "message": "Welcome!" - }, - "welcome_dd4e7151": { - "message": "Welcome" - }, - "westeurope_cabf9688": { - "message": "westeurope" - }, - "westus_dc50d800": { - "message": "westus" - }, - "what_can_the_user_accomplish_through_this_conversa_7ddb03a1": { - "message": "What can the user accomplish through this conversation? For example, BookATable, OrderACoffee etc." - }, - "what_is_the_name_of_the_custom_event_b28a7b3": { - "message": "What is the name of the custom event?" - }, - "what_is_the_name_of_this_trigger_2642266e": { - "message": "What is the name of this trigger" - }, - "what_is_the_name_of_this_trigger_luis_17b60a23": { - "message": "What is the name of this trigger (LUIS)" - }, - "what_is_the_name_of_this_trigger_regex_f77376d7": { - "message": "What is the name of this trigger (RegEx)" - }, - "what_is_the_name_of_your_bot_a571c565": { - "message": "What is the name of your bot?" - }, - "what_is_the_type_of_this_trigger_d2701744": { - "message": "What is the type of this trigger?" - }, - "what_your_bot_says_to_the_user_this_is_a_template__a8d2266d": { - "message": "What your bot says to the user. This is a template used to create the outgoing message. It can include language generation rules, properties from memory, and other features.\n\nFor example, to define variations that will be chosen at random, write:\n- hello\n- hi" - }, - "what_your_bot_says_to_the_user_visit_a_target_blan_7735479": { - "message": "What your Bot says to the user. Visit the documentation a reference of capabilities." - }, - "when_deleting_a_language_only_the_content_will_be__8f7f8dee": { - "message": "When deleting a language, only the content will be removed. The flow and logic of the conversation and dialog will remain functional." - }, - "when_done_switch_to_the_newly_created_language_and_862b7dd9": { - "message": "When done, switch to the newly created language and start the (manual) translation process." - }, - "when_multiple_people_are_working_with_models_you_w_32b48099": { - "message": "When multiple people are working with models you want to be able to work with models independently from each other tied to the source control." - }, - "which_activity_type_18333457": { - "message": "Which activity type?" - }, - "which_bot_do_you_want_to_open_974bb1e5": { - "message": "Which bot do you want to open?" - }, - "which_event_6e655d2b": { - "message": "Which event?" - }, - "yes_dde87d5": { - "message": "Yes" - }, - "you_are_about_to_publish_your_bot_to_the_profile_b_79a6a226": { - "message": "You are about to publish your bot to the profile below. Do you want to proceed?" - }, - "you_can_create_a_new_bot_from_scratch_with_compose_1486288c": { - "message": "You can create a new bot from scratch with Composer, or start with a template." - }, - "you_can_define_and_manage_b_intents_b_here_each_in_721b8a0c": { - "message": "You can define and manage intents here. Each intent describes a particular user intention through utterances (i.e. user says). Intents are often triggers of your bot." - }, - "you_can_manage_all_bot_responses_here_make_good_us_5e6e1953": { - "message": "You can manage all bot responses here. Make good use of the templates to create sophisticated response logic based on your own needs." - }, - "you_do_not_have_permission_to_save_bots_here_56cc10c7": { - "message": "You do not have permission to save bots here" - }, - "your_bot_creation_journey_on_composer_131c1a8b": { - "message": "Your bot creation journey on Composer" - }, - "your_bot_is_using_luis_and_qna_for_natural_languag_53830684": { - "message": "Your bot is using LUIS and QNA for natural language understanding." - }, - "your_knowledge_base_is_ready_6ecc1871": { - "message": "Your knowledge base is ready!" - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/Composer/packages/ui-plugins/composer/package.json b/Composer/packages/ui-plugins/composer/package.json index c59aa6fc15..6ca898232d 100644 --- a/Composer/packages/ui-plugins/composer/package.json +++ b/Composer/packages/ui-plugins/composer/package.json @@ -24,7 +24,7 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -36,10 +36,10 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message-cli": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { "lodash": "^4.17.19" } -} \ No newline at end of file +} diff --git a/Composer/packages/ui-plugins/cross-trained/package.json b/Composer/packages/ui-plugins/cross-trained/package.json index d60cf9b9b6..aa9d055c37 100644 --- a/Composer/packages/ui-plugins/cross-trained/package.json +++ b/Composer/packages/ui-plugins/cross-trained/package.json @@ -22,7 +22,7 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -30,7 +30,7 @@ "@bfc/extension-client": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/example/package.json b/Composer/packages/ui-plugins/example/package.json index 3322fbc7c6..717b23aa47 100644 --- a/Composer/packages/ui-plugins/example/package.json +++ b/Composer/packages/ui-plugins/example/package.json @@ -23,7 +23,7 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@emotion/core": "^10.0.27", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/lg/package.json b/Composer/packages/ui-plugins/lg/package.json index 142e105925..28f861c712 100644 --- a/Composer/packages/ui-plugins/lg/package.json +++ b/Composer/packages/ui-plugins/lg/package.json @@ -23,7 +23,7 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -32,7 +32,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/luis/package.json b/Composer/packages/ui-plugins/luis/package.json index 6eeb2588bf..3d31df3d17 100644 --- a/Composer/packages/ui-plugins/luis/package.json +++ b/Composer/packages/ui-plugins/luis/package.json @@ -24,7 +24,7 @@ "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", "@bfc/shared": "*", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/prompts/package.json b/Composer/packages/ui-plugins/prompts/package.json index 8d4d6e0840..c7c02f27d6 100644 --- a/Composer/packages/ui-plugins/prompts/package.json +++ b/Composer/packages/ui-plugins/prompts/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/schema-editor/package.json b/Composer/packages/ui-plugins/schema-editor/package.json index caee894f71..2cb8856a81 100644 --- a/Composer/packages/ui-plugins/schema-editor/package.json +++ b/Composer/packages/ui-plugins/schema-editor/package.json @@ -24,7 +24,7 @@ "@bfc/adaptive-form": "*", "@bfc/code-editor": "*", "format-message": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -34,7 +34,7 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/select-dialog/package.json b/Composer/packages/ui-plugins/select-dialog/package.json index b507dac159..87ba3cccde 100644 --- a/Composer/packages/ui-plugins/select-dialog/package.json +++ b/Composer/packages/ui-plugins/select-dialog/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/package.json b/Composer/packages/ui-plugins/select-skill-dialog/package.json index c05e3d53fe..cbcd570eaf 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/package.json +++ b/Composer/packages/ui-plugins/select-skill-dialog/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.1", + "react": "16.13.0", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/yarn.lock b/Composer/yarn.lock index bee4dd509f..6bd0f20f06 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -16018,10 +16018,10 @@ react-timeago@^4.4.0: resolved "https://botbuilder.myget.org/F/botbuilder-tools-daily/npm/react-timeago/-/react-timeago-4.4.0.tgz#4520dd9ba63551afc4d709819f52b14b9343ba2b" integrity sha1-RSDdm6Y1Ua/E1wmBn1KxS5NDuis= -react@16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== +react@16.13.0: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7" + integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" From 98c60d697829d8b06bb186614f67345cb713f1bd Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 2 Oct 2020 12:57:15 -0700 Subject: [PATCH 015/100] Revert "Revert usage of React 16.13.1 to 16.13.0" This reverts commit 9aa46ca1775fd6c53c724355563a040870ae2dba. --- Composer/packages/adaptive-flow/package.json | 4 +- Composer/packages/adaptive-form/package.json | 4 +- Composer/packages/client/package.json | 2 +- .../src/recoilModel/selectors/project.ts | 1 + .../packages/extension-client/package.json | 4 +- Composer/packages/intellisense/package.json | 2 +- .../packages/lib/code-editor/package.json | 6 +- Composer/packages/lib/shared/package.json | 4 +- Composer/packages/lib/ui-shared/package.json | 4 +- .../packages/server/src/locales/en-US.json | 2397 ++++++++++++++++- .../packages/ui-plugins/composer/package.json | 6 +- .../ui-plugins/cross-trained/package.json | 4 +- .../packages/ui-plugins/example/package.json | 4 +- Composer/packages/ui-plugins/lg/package.json | 4 +- .../packages/ui-plugins/luis/package.json | 4 +- .../packages/ui-plugins/prompts/package.json | 4 +- .../ui-plugins/schema-editor/package.json | 4 +- .../ui-plugins/select-dialog/package.json | 4 +- .../select-skill-dialog/package.json | 4 +- Composer/yarn.lock | 8 +- 20 files changed, 2435 insertions(+), 39 deletions(-) diff --git a/Composer/packages/adaptive-flow/package.json b/Composer/packages/adaptive-flow/package.json index 2d69e0b49d..adde0e7f0e 100644 --- a/Composer/packages/adaptive-flow/package.json +++ b/Composer/packages/adaptive-flow/package.json @@ -42,7 +42,7 @@ }, "peerDependencies": { "format-message": "^6.2.3", - "react": "16.13.0" + "react": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", @@ -50,7 +50,7 @@ "@types/react": "16.9.23", "format-message": "^6.2.3", "json-loader": "^0.5.7", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1", "ts-loader": "7.0.4", "tsconfig-paths-webpack-plugin": "^3.2.0" diff --git a/Composer/packages/adaptive-form/package.json b/Composer/packages/adaptive-form/package.json index 18484c542d..2a91119915 100644 --- a/Composer/packages/adaptive-form/package.json +++ b/Composer/packages/adaptive-form/package.json @@ -26,7 +26,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -37,7 +37,7 @@ "@types/lodash": "^4.14.149", "@types/react": "16.9.23", "format-message": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 860313b333..b9417f2435 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -51,7 +51,7 @@ "query-string": "^6.8.2", "react-measure": "^2.3.0", "re-resizable": "^6.3.2", - "react": "16.13.0", + "react": "16.13.1", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.3", "react-dom": "16.13.1", diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index c568cf99c9..9f976b162f 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -39,6 +39,7 @@ export const botProjectSpaceSelector = selector({ const botNameId = get(botNameIdentifierState(projectId)); return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); + console.log(result); return result; }, }); diff --git a/Composer/packages/extension-client/package.json b/Composer/packages/extension-client/package.json index 8e67eb418e..0948d57580 100644 --- a/Composer/packages/extension-client/package.json +++ b/Composer/packages/extension-client/package.json @@ -11,13 +11,13 @@ "clean": "rimraf lib" }, "peerDependencies": { - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { "@bfc/test-utils": "*", "@types/react": "16.9.23", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1", "rimraf": "^3.0.2", "typescript": "3.9.2" diff --git a/Composer/packages/intellisense/package.json b/Composer/packages/intellisense/package.json index dcb25fb93a..0dcb7ef5b1 100644 --- a/Composer/packages/intellisense/package.json +++ b/Composer/packages/intellisense/package.json @@ -10,7 +10,7 @@ }, "peerDependencies": { "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/lib/code-editor/package.json b/Composer/packages/lib/code-editor/package.json index 3f8697b6b1..9a149f06b9 100644 --- a/Composer/packages/lib/code-editor/package.json +++ b/Composer/packages/lib/code-editor/package.json @@ -24,7 +24,7 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -43,7 +43,7 @@ "mini-css-extract-plugin": "^0.6.0", "postcss-loader": "^3.0.0", "react-dom": "16.13.1", - "react": "16.13.0", + "react": "16.13.1", "rimraf": "^2.6.3", "style-loader": "^0.23.1", "ts-loader": "7.0.4", @@ -66,4 +66,4 @@ "reconnecting-websocket": "^3.2.2", "vscode-ws-jsonrpc": "^0.1.1" } -} +} \ No newline at end of file diff --git a/Composer/packages/lib/shared/package.json b/Composer/packages/lib/shared/package.json index b9f769e089..c9a10931f0 100644 --- a/Composer/packages/lib/shared/package.json +++ b/Composer/packages/lib/shared/package.json @@ -21,7 +21,7 @@ "watch": "yarn build:ts --watch" }, "peerDependencies": { - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -32,7 +32,7 @@ "autoprefixer": "^9.5.1", "copyfiles": "^2.1.0", "office-ui-fabric-react": "^7.88.1", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1", "rimraf": "^2.6.3" }, diff --git a/Composer/packages/lib/ui-shared/package.json b/Composer/packages/lib/ui-shared/package.json index 5e77e27442..5ad003d57b 100644 --- a/Composer/packages/lib/ui-shared/package.json +++ b/Composer/packages/lib/ui-shared/package.json @@ -24,7 +24,7 @@ "@emotion/core": "^10.0.27", "@emotion/styled": "^10.0.27", "@bfc/extension-client": "*", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 9e26dfeeb6..5c8444ead3 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -1 +1,2396 @@ -{} \ No newline at end of file +{ + "0_bytes_a1e1cdb3": { + "message": "0 Bytes" + }, + "5_minute_intro_7ea06d2b": { + "message": "5 Minute Intro" + }, + "ErrorInfo_part1": { + "message": "An error occured in the form editor!" + }, + "ErrorInfo_part2": { + "message": "This is likely due to malformed data or missing functionality in Composer." + }, + "ErrorInfo_part3": { + "message": "Try navigating to another node in the visual editor." + }, + "a_dialog_file_must_have_a_name_123ff67d": { + "message": "a dialog file must have a name" + }, + "a_minimap_gives_an_overview_of_your_source_code_fo_9a897f4f": { + "message": "A minimap gives an overview of your source code for quick navigation and code understanding." + }, + "a_newer_version_of_the_provisioning_scripts_has_be_85d3ad94": { + "message": "A newer version of the provisioning scripts has been found, and this project can be updated to the latest." + }, + "a_profile_with_that_name_already_exists_7c559958": { + "message": "A profile with that name already exists." + }, + "a_subscription_key_is_created_when_you_create_a_qn_37a6926f": { + "message": "A subscription key is created when you create a QnA Maker resource." + }, + "a_valid_url_should_start_with_http_or_https_d24b3591": { + "message": "A valid url should start with http:// or https://" + }, + "about_70c18bba": { + "message": "About" + }, + "access_external_resources_7e37fe21": { + "message": "Access external resources" + }, + "action_created_f80af9a0": { + "message": "Action created" + }, + "action_deleted_a10c002b": { + "message": "Action deleted" + }, + "action_focused_400ee4af": { + "message": "Action focused" + }, + "action_unfocused_18a2800e": { + "message": "Action unfocused" + }, + "actions_copied_2821ab27": { + "message": "Actions copied" + }, + "actions_created_6c5acc11": { + "message": "Actions created" + }, + "actions_cut_929f4c37": { + "message": "Actions cut" + }, + "actions_define_b_how_the_bot_responds_b_to_a_certa_890a71f4": { + "message": "Actions define how the bot responds to a certain trigger." + }, + "actions_deleted_355c359a": { + "message": "Actions deleted" + }, + "actions_disabled_c1accfb6": { + "message": "Actions disabled" + }, + "actions_efcde10d": { + "message": "Actions" + }, + "actions_enabled_8e9ec02d": { + "message": "Actions enabled" + }, + "actions_moved_d7777b29": { + "message": "Actions moved" + }, + "activities_125193f7": { + "message": "Activities" + }, + "activity_13915493": { + "message": "Activity" + }, + "activity_received_2f20fa9d": { + "message": "Activity received" + }, + "adaptive_dialog_61a05dde": { + "message": "Adaptive dialog" + }, + "add_8523c19b": { + "message": "Add" + }, + "add_a_new_key_5c208c29": { + "message": "Add a new key" + }, + "add_a_new_skill_dialog_aaaafa9c": { + "message": "Add a new Skill Dialog" + }, + "add_a_new_trigger_a82d3db8": { + "message": "Add a new trigger" + }, + "add_a_new_value_24ca14ac": { + "message": "Add a new value" + }, + "add_a_publish_profile_fca2263a": { + "message": "Add a publish profile" + }, + "add_a_skill_46d2b71c": { + "message": "Add a skill" + }, + "add_a_welcome_message_9e1480b2": { + "message": "Add a welcome message" + }, + "add_additional_url_bdfac25d": { + "message": "Add additional URL" + }, + "add_alternative_phrasing_47234f77": { + "message": "add alternative phrasing" + }, + "add_an_intent_trigger_a9acc149": { + "message": "Add an intent trigger" + }, + "add_custom_runtime_6b73dc44": { + "message": "Add custom runtime" + }, + "add_language_75dee49e": { + "message": "Add language" + }, + "add_multiple_comma_separated_synonyms_2639283f": { + "message": "Add multiple comma-separated synonyms" + }, + "add_new_dialog_25f12c9": { + "message": "Add new dialog" + }, + "add_new_extension_19b82b77": { + "message": "Add new extension" + }, + "add_new_knowledge_base_on_displayname_7f44609c": { + "message": " Add new knowledge base on { displayName }" + }, + "add_new_profile_47b225e6": { + "message": "Add new profile" + }, + "add_new_propertyname_bedf7dc6": { + "message": "Add new { propertyName }" + }, + "add_new_trigger_on_displayname_2aaf6e4c": { + "message": "Add new trigger on { displayName }" + }, + "add_new_validation_rule_here_eb675ccf": { + "message": "Add new validation rule here" + }, + "add_qna_pair_bcb1624": { + "message": "Add QnA Pair" + }, + "add_some_example_phrases_to_trigger_this_intent_pl_568eaf51": { + "message": "> add some example phrases to trigger this intent:\n> - please tell me the weather\n> - what is the weather like in '{'city=Seattle'}'\n\n> entity definitions:\n> @ ml city" + }, + "add_some_expected_user_responses_please_remind_me__31dc5c07": { + "message": "> add some expected user responses:\n> - Please remind me to '{'itemTitle=buy milk'}'\n> - remind me to '{'itemTitle'}'\n> - add '{'itemTitle'}' to my todo list\n>\n> entity definitions:\n> @ ml itemTitle\n" + }, + "add_welcome_message_49d9ded9": { + "message": "Add welcome message" + }, + "advanced_events_2cbfa47d": { + "message": "Advanced Events" + }, + "all_4321c3a1": { + "message": "All" + }, + "all_language_generation_files_ef778f24": { + "message": "all language generation files" + }, + "all_language_understanding_files_714b33eb": { + "message": "all language understanding files" + }, + "all_profiles_8321b1aa": { + "message": "All profiles" + }, + "all_qna_files_349d7fe1": { + "message": "all qna files" + }, + "an_authoring_key_is_created_automatically_when_you_21cf77aa": { + "message": "An authoring key is created automatically when you create a LUIS account." + }, + "answer_4620913f": { + "message": "Answer" + }, + "answer_is_answer_799483f0": { + "message": "Answer is { answer }" + }, + "any_constant_or_expression_to_evaluate_ba2017b1": { + "message": "Any constant or expression to evaluate." + }, + "any_or_expression_acad7d37": { + "message": "Any or expression" + }, + "append_choices_35c45a2d": { + "message": "Append choices" + }, + "application_language_87691b6": { + "message": "Application Language" + }, + "application_language_f100f3e0": { + "message": "Application language" + }, + "application_settings_39e840c6": { + "message": "Application Settings" + }, + "application_updates_bdf5f8b6": { + "message": "Application Updates" + }, + "apr_9_2020_3c8b47d7": { + "message": "Apr 9, 2020" + }, + "are_you_sure_you_want_to_exit_the_onboarding_produ_c2de1b23": { + "message": "Are you sure you want to exit the Onboarding Product Tour? You can restart the tour in the onboarding settings." + }, + "are_you_sure_you_want_to_stop_current_runtime_and__a5f883b2": { + "message": "Are you sure you want to stop current runtime and replace them?" + }, + "array_643947ee": { + "message": "Array" + }, + "array_constant_8829b2ec": { + "message": "Array constant." + }, + "array_or_expression_c52b2ecc": { + "message": "Array or expression" + }, + "array_or_expression_to_evaluate_87d9328b": { + "message": "Array or expression to evaluate." + }, + "ask_a_question_92ef7e0c": { + "message": "Ask a question" + }, + "attachment_input_e0ece49c": { + "message": "Attachment Input" + }, + "australia_6a25c95b": { + "message": "australia" + }, + "authoring_canvas_18802e39": { + "message": "Authoring canvas" + }, + "authoring_region_to_use_e_g_westus_westeurope_aust_d43d5245": { + "message": "Authoring region to use (e.g. westus, westeurope, australiaeast)" + }, + "authoring_region_to_use_westus_qna_maker_resource__4588c2f9": { + "message": "Authoring region to use (westus) (QnA maker resource location)" + }, + "auto_update_86eb33b0": { + "message": "Auto update" + }, + "available_skills_95c114ac": { + "message": "Available Skills" + }, + "been_used_5daccdb2": { + "message": "Been used" + }, + "begin_a_new_dialog_60249bd8": { + "message": "Begin a new dialog" + }, + "begin_a_remote_skill_dialog_93e47189": { + "message": "Begin a remote skill dialog." + }, + "begin_dialog_12e2becf": { + "message": "Begin Dialog" + }, + "begin_dialog_event_285bc650": { + "message": "Begin dialog event" + }, + "begindialog_a5594562": { + "message": "BeginDialog" + }, + "ben_brown_99c12d19": { + "message": "Ben Brown" + }, + "boolean_6000988a": { + "message": "Boolean" + }, + "boolean_condition_b65450ca": { + "message": "Boolean condition" + }, + "boolean_constant_7d3fcbf6": { + "message": "Boolean constant" + }, + "boolean_constant_8d950af8": { + "message": "Boolean constant." + }, + "boolean_constant_or_expression_to_evaluate_86d77849": { + "message": "Boolean constant or expression to evaluate." + }, + "boolean_field_602934c9": { + "message": "boolean field" + }, + "boolean_or_expression_6bd88208": { + "message": "Boolean or expression" + }, + "boolean_value_98d39ea1": { + "message": "Boolean value." + }, + "bot_asks_5e9f0202": { + "message": "Bot Asks" + }, + "bot_framework_composer_enables_developers_and_mult_ce0e42a9": { + "message": "Bot Framework Composer enables developers and multi-disciplinary teams to build all kinds of conversational experiences, using the latest components from the Bot Framework: SDK, LG, LU, and declarative file formats, all without writing code." + }, + "bot_framework_composer_fae721be": { + "message": "Bot Framework Composer" + }, + "bot_framework_composer_icon_gray_fa72d3d6": { + "message": "bot framework composer icon gray" + }, + "bot_framework_composer_is_a_visual_authoring_canva_c3947d91": { + "message": "Bot Framework Composer is a visual authoring canvas for building bots and other types of conversational application with the Microsoft Bot Framework technology stack. With Composer you will find everything you need to build a modern, state-of-the-art conversational experience." + }, + "bot_framework_composer_is_an_open_source_visual_au_2be2e02b": { + "message": "Bot Framework Composer is an open-source visual authoring canvas for developers and multi-disciplinary teams to build bots. Composer integrates LUIS and QnA Maker, and allows sophisticated composition of bot replies using language generation." + }, + "bot_framework_provides_the_most_comprehensive_expe_e34a7f5d": { + "message": "Bot Framework provides the most comprehensive experience for building conversational applications." + }, + "bot_language_6cf30c2": { + "message": "Bot language" + }, + "bot_language_active_7cf9dc78": { + "message": "Bot language (active)" + }, + "bot_name_is_botname_a28c2d05": { + "message": "Bot name is { botName }" + }, + "bot_responses_4617b4a2": { + "message": "Bot responses" + }, + "bot_responses_c4e63601": { + "message": "Bot Responses" + }, + "bot_settings_3751e1b2": { + "message": "Bot settings" + }, + "bot_settings_ce2783e4": { + "message": "Bot Settings" + }, + "branch_if_else_391e5681": { + "message": "Branch: If/else" + }, + "branch_if_else_992cf9bf": { + "message": "Branch: If/Else" + }, + "branch_if_else_f6a36f1d": { + "message": "Branch: if/else" + }, + "branch_switch_multiple_options_95c6a326": { + "message": "Branch: Switch (multiple options)" + }, + "break_out_of_loop_ab30157c": { + "message": "Break out of loop" + }, + "build_your_first_bot_f9c3e427": { + "message": "Build your first bot" + }, + "bytes_74acea97": { + "message": "Bytes" + }, + "calculating_17b21be7": { + "message": "Calculating..." + }, + "cancel_all_active_dialogs_335b1623": { + "message": "Cancel all active dialogs" + }, + "cancel_all_dialogs_32144c45": { + "message": "Cancel All Dialogs" + }, + "cancel_caeb1e68": { + "message": "Cancel" + }, + "cancel_dialog_event_cc671dee": { + "message": "Cancel dialog event" + }, + "change_recognizer_3145b93d": { + "message": "Change Recognizer" + }, + "check_for_updates_and_install_them_automatically_50337340": { + "message": "Check for updates and install them automatically." + }, + "choice_input_f75a2353": { + "message": "Choice Input" + }, + "choice_name_fe8411f4": { + "message": "Choice Name" + }, + "choose_a_location_for_your_new_bot_project_e979f2d5": { + "message": "Choose a location for your new bot project." + }, + "choose_how_to_create_your_bot_a97f7b3e": { + "message": "Choose how to create your bot" + }, + "choose_one_2c4277df": { + "message": "Choose One" + }, + "chris_whitten_11df1f35": { + "message": "Chris Whitten" + }, + "click_on_the_b_add_b_button_in_the_toolbar_and_sel_4daf351a": { + "message": "Click on the Add button in the toolbar, and select Add a new trigger. In the Create a trigger wizard, set the Trigger Type to Intent recognized and configure the Trigger Name and Trigger Phrases. Then add actions in the Visual Editor." + }, + "click_the_b_add_b_button_in_the_toolbar_and_select_79001156": { + "message": "Click the Add button in the toolbar, and select Add a new trigger from the dropdown menu." + }, + "click_to_sort_by_file_type_1b0c9bd": { + "message": "Click to sort by file type" + }, + "close_d634289d": { + "message": "Close" + }, + "collapse_34080b4d": { + "message": "Collapse" + }, + "collapse_navigation_17228b95": { + "message": "Collapse Navigation" + }, + "comment_7ef1428e": { + "message": "Comment" + }, + "component_stacktrace_e24b1983": { + "message": "Component Stacktrace:" + }, + "composer_cannot_yet_translate_your_bot_automatical_2d54081b": { + "message": "Composer cannot yet translate your bot automatically.\nTo create a translation manually, Composer will create a copy of your bot’s content with the name of the additional language. This content can then be translated without affecting the original bot logic or flow and you can switch between languages to ensure the responses are correctly and appropriately translated." + }, + "composer_introduction_98a93701": { + "message": "Composer introduction" + }, + "composer_is_up_to_date_9118257d": { + "message": "Composer is up to date." + }, + "composer_logo_ba2048a0": { + "message": "Composer Logo" + }, + "composer_needs_net_core_sdk_46e2a8ae": { + "message": "Composer needs .NET Core SDK" + }, + "composer_will_restart_88ee8dc3": { + "message": "Composer will restart." + }, + "composer_will_update_the_next_time_you_close_the_a_d74264a1": { + "message": "Composer will update the next time you close the app." + }, + "conditionalselector_ed2031f0": { + "message": "ConditionalSelector" + }, + "configure_composer_to_start_your_bot_using_runtime_fe37dadf": { + "message": "Configure Composer to start your bot using runtime code you can customize and control." + }, + "configures_default_language_model_to_use_if_there__f09f1acd": { + "message": "Configures default language model to use if there is no culture code in the file name (Default: en-us)" + }, + "confirm_6556b3a6": { + "message": "Confirm" + }, + "confirm_choices_db8e99fb": { + "message": "Confirm Choices" + }, + "confirm_input_bf996e7a": { + "message": "Confirm Input" + }, + "confirmation_fec87d65": { + "message": "Confirmation" + }, + "confirmation_modal_must_have_a_title_b0816e0b": { + "message": "Confirmation modal must have a title." + }, + "congratulations_your_model_is_successfully_publish_52ebc297": { + "message": "Congratulations! Your model is successfully published." + }, + "connect_to_a_new_skill_34d582ab": { + "message": "Connect to a new skill" + }, + "connect_to_a_skill_53c9dff0": { + "message": "Connect to a skill" + }, + "connect_to_qna_knowledgebase_4b324132": { + "message": "Connect to QnA Knowledgebase" + }, + "continue_loop_22635585": { + "message": "Continue loop" + }, + "conversation_ended_a8bd37dd": { + "message": "Conversation ended" + }, + "conversation_invoked_e960884e": { + "message": "Conversation invoked" + }, + "conversationupdate_activity_9e94bff5": { + "message": "ConversationUpdate activity" + }, + "copy_9748f9f": { + "message": "Copy" + }, + "copy_content_for_translation_7affbcbb": { + "message": "Copy content for translation" + }, + "could_not_connect_to_storage_50411de0": { + "message": "Could not connect to storage." + }, + "could_not_init_plugin_1f1c29cd": { + "message": "Could not init plugin" + }, + "couldn_t_complete_the_update_a337a359": { + "message": "Couldn''t complete the update:" + }, + "create_132b3be1": { + "message": "Create" + }, + "create_a_bot_project_73e6ce33": { + "message": "Create a bot project" + }, + "create_a_condition_8686fd5": { + "message": "Create a condition" + }, + "create_a_dialog_9f6c8618": { + "message": "Create a dialog" + }, + "create_a_name_for_the_project_which_will_be_used_t_57e9b690": { + "message": "Create a name for the project which will be used to name the application: (projectname-environment-LUfilename)" + }, + "create_a_new_dialog_21d84b82": { + "message": "Create a new dialog" + }, + "create_a_new_skill_manifest_or_select_which_one_yo_a97e9616": { + "message": "Create a new skill manifest or select which one you want to edit" + }, + "create_a_trigger_40e74743": { + "message": "Create a trigger" + }, + "create_bot_from_template_or_scratch_92f0fefa": { + "message": "Create bot from template or scratch?" + }, + "create_copy_to_translate_bot_content_efc872c": { + "message": "Create copy to translate bot content" + }, + "create_folder_error_38aa86f5": { + "message": "Create Folder Error" + }, + "create_from_knowledge_base_qna_maker_7422486": { + "message": "Create from knowledge base (QnA Maker)" + }, + "create_from_qna_7aa9dcbb": { + "message": "Create from QnA" + }, + "create_from_scratch_485c3045": { + "message": "Create from scratch" + }, + "create_from_template_87e12c94": { + "message": "Create from template" + }, + "create_knowledge_base_db6d66c4": { + "message": "Create knowledge base" + }, + "create_knowledge_base_from_scratch_afe4d2a2": { + "message": "Create knowledge base from scratch" + }, + "create_new_empty_bot_21cf0ea3": { + "message": "Create new empty bot" + }, + "create_new_folder_19d3faa4": { + "message": "Create new folder" + }, + "create_new_knowledge_base_e14d07a5": { + "message": "Create New Knowledge Base" + }, + "create_or_edit_skill_manifest_8ad98da9": { + "message": "Create or edit skill manifest" + }, + "creating_your_knowledge_base_ef4f9872": { + "message": "Creating your knowledge base" + }, + "current_40c0812f": { + "message": " - Current" + }, + "custom_actions_5d396747": { + "message": "Custom Actions" + }, + "custom_events_d6f0e45b": { + "message": "Custom events" + }, + "custom_name_optional_60ee6a1a": { + "message": "Custom name (optional)" + }, + "custom_recognizer_951bab90": { + "message": "Custom recognizer" + }, + "cut_c8c92681": { + "message": "Cut" + }, + "data_loading_4c9bb9f6": { + "message": "Data loading..." + }, + "date_ee500367": { + "message": "Date" + }, + "date_modified_18beced9": { + "message": "Date modified" + }, + "date_modified_e1c8ac8f": { + "message": "Date Modified" + }, + "date_or_time_d30bcc7d": { + "message": "Date or time" + }, + "date_time_input_2416ffc1": { + "message": "Date Time Input" + }, + "debug_break_46cb5adb": { + "message": "Debug Break" + }, + "debug_break_870a75df": { + "message": "Debug break" + }, + "debugging_options_20e2e9da": { + "message": "Debugging options" + }, + "decrement_by_step_9b6c2fa3": { + "message": "decrement by { step }" + }, + "default_language_486a558d": { + "message": "Default language" + }, + "default_language_b11c37db": { + "message": "Default Language" + }, + "default_recognizer_9c06c1a3": { + "message": "Default recognizer" + }, + "define_conversation_objective_146d1cc6": { + "message": "Define conversation objective" + }, + "defined_in_475568fb": { + "message": "Defined in:" + }, + "definition_not_found_for_defname_7194fd07": { + "message": "Definition not found for { defName }" + }, + "delete_a6efa79d": { + "message": "Delete" + }, + "delete_a_property_f2d70f79": { + "message": "Delete a property" + }, + "delete_activity_6d881872": { + "message": "Delete activity" + }, + "delete_bot_73586104": { + "message": "Delete Bot" + }, + "delete_language_1527609d": { + "message": "Delete language" + }, + "delete_properties_8bc77b42": { + "message": "Delete Properties" + }, + "delete_properties_c49a7892": { + "message": "Delete properties" + }, + "delete_property_b3786fa0": { + "message": "Delete Property" + }, + "describe_your_skill_88554792": { + "message": "Describe your skill" + }, + "description_436c48d7": { + "message": "Description" + }, + "design_51b2812a": { + "message": "Design" + }, + "dialog_68ba69ba": { + "message": "(Dialog)" + }, + "dialog_cancelled_767b512a": { + "message": "Dialog cancelled" + }, + "dialog_data_61d5539b": { + "message": "Dialog data" + }, + "dialog_dialogid_not_found_5e8214c3": { + "message": "dialog { dialogId } not found" + }, + "dialog_events_f1b2e2a0": { + "message": "Dialog events" + }, + "dialog_input_schema_c7864fbd": { + "message": "Dialog input schema." + }, + "dialog_interface_398bc493": { + "message": "Dialog Interface" + }, + "dialog_management_2980578": { + "message": "Dialog management" + }, + "dialog_opened_f6392b1": { + "message": "Dialog opened" + }, + "dialog_output_schema_acfe2186": { + "message": "Dialog output schema." + }, + "dialog_started_912507c": { + "message": "Dialog started" + }, + "dialogfactory_missing_schema_5c3255c4": { + "message": "DialogFactory missing schema." + }, + "dialognum_plural_0_no_dialogs_1_one_dialog_other_d_1b86909b": { + "message": "{ dialogNum, plural,\n =0 {No dialogs}\n =1 {One dialog}\n other {# dialogs}\n} have been found.\n { dialogNum, select,\n 0 {}\n other {Press down arrow key to navigate the search results}\n}" + }, + "disable_a5c05db3": { + "message": "Disable" + }, + "display_lines_that_extends_beyond_the_width_of_the_9e500f3c": { + "message": "Display lines that extends beyond the width of the editor on the next line." + }, + "do_you_wish_to_continue_96469eaf": { + "message": "Do you wish to continue?" + }, + "does_not_exist_3a34b418": { + "message": "Does Not Exist" + }, + "done_1a4a010a": { + "message": "Done!" + }, + "done_54e3d4b6": { + "message": "Done" + }, + "download_now_and_install_when_you_close_composer_e241ed74": { + "message": "Download now and install when you close Composer." + }, + "downloading_bb6fb34b": { + "message": "Downloading..." + }, + "duplicate_dialog_name_824f9fce": { + "message": "Duplicate dialog name" + }, + "duplicate_fields_9fd0d3c2": { + "message": "duplicate fields" + }, + "duplicate_name_d295a09d": { + "message": "Duplicate name" + }, + "duplicate_skill_manifest_url_74d85839": { + "message": "Duplicate skill manifest Url" + }, + "duplicate_skill_name_d9f6eb8d": { + "message": "Duplicate skill name" + }, + "duplicated_intents_recognized_d3908424": { + "message": "Duplicated intents recognized" + }, + "early_adopters_e8db7999": { + "message": "Early adopters" + }, + "edit_a_publish_profile_30ebab3e": { + "message": "Edit a publish profile" + }, + "edit_a_skill_5665d9ac": { + "message": "Edit a skill" + }, + "edit_actions_b38e9fac": { + "message": "Edit Actions" + }, + "edit_an_array_property_5d886011": { + "message": "Edit an array property" + }, + "edit_array_4ab37c8": { + "message": "Edit Array" + }, + "edit_c5fbea07": { + "message": "Edit" + }, + "edit_displayname_dialog_description_986a7d60": { + "message": "Edit { displayName } dialog description" + }, + "edit_in_json_75d0d754": { + "message": "Edit in JSON" + }, + "edit_mode_7e07cfdf": { + "message": "Edit mode" + }, + "edit_property_dd6a1172": { + "message": "Edit Property" + }, + "ejecting_runtime_f6c90614": { + "message": "Ejecting runtime..." + }, + "emit_a_custom_event_78cf318b": { + "message": "Emit a custom event" + }, + "emit_a_telemetry_track_event_e2442842": { + "message": "Emit a telemetry track event" + }, + "emit_a_trace_event_f653ae84": { + "message": "Emit a trace event" + }, + "emit_event_32aa6583": { + "message": "Emit Event" + }, + "enable_6f5d1328": { + "message": "Enable" + }, + "enable_line_numbers_to_refer_to_code_lines_by_numb_e5ba66ea": { + "message": "Enable line numbers to refer to code lines by number." + }, + "enabled_ba7cab66": { + "message": "Enabled" + }, + "end_dialog_8f562a4c": { + "message": "End Dialog" + }, + "end_this_dialog_3ed0d50b": { + "message": "End this dialog" + }, + "end_turn_6ab71cea": { + "message": "End Turn" + }, + "end_turn_ca85b3d4": { + "message": "End turn" + }, + "endofconversation_activity_4aa21306": { + "message": "EndOfConversation activity" + }, + "english_us_1d884ccd": { + "message": "English (US)" + }, + "enter_a_manifest_url_to_add_a_new_skill_to_your_bo_57e9d660": { + "message": "Enter a manifest url to add a new skill to your bot." + }, + "enter_a_question_74f2e045": { + "message": "Enter a question" + }, + "enter_an_answer_84152a83": { + "message": "Enter an answer" + }, + "environment_68aed6d3": { + "message": "Environment" + }, + "equals_expression_47199f4e": { + "message": "Equals Expression" + }, + "error_98e81528": { + "message": "Error" + }, + "error_afac7133": { + "message": "Error:" + }, + "error_event_c079b608": { + "message": "Error event" + }, + "error_func_does_not_have_an_evaluator_it_s_not_a_b_8c660980": { + "message": "Error: { func } does not have an evaluator, it''s not a built-in function or a custom function" + }, + "error_in_ui_schema_for_title_errormsg_options_7f3c22f2": { + "message": "Error in UI schema for { title }: { errorMsg }\n{ options }" + }, + "error_occured_ejecting_runtime_76bb48e7": { + "message": "Error occured ejecting runtime!" + }, + "error_occurred_5549a6b4": { + "message": "Error occurred" + }, + "error_processing_schema_2c707cf3": { + "message": "Error Processing Schema" + }, + "event_activity_2067a94b": { + "message": "Event activity" + }, + "event_created_e9e05afc": { + "message": "Event created" + }, + "event_focused_51e447f3": { + "message": "Event focused" + }, + "event_received_457f99d6": { + "message": "Event received" + }, + "events_cf7a8c50": { + "message": "Events" + }, + "example_bot_list_9be1d563": { + "message": "Example bot list" + }, + "examples_c435f08c": { + "message": "Examples" + }, + "existing_files_in_scripts_folder_will_be_overwritt_afa8d787": { + "message": "Existing files in scripts/folder will be overwritten. Are you sure you want to continue?" + }, + "expand_2f2fadbd": { + "message": "Expand" + }, + "expand_navigation_20330d1d": { + "message": "Expand Navigation" + }, + "expected_responses_intent_intentname_44b051c": { + "message": "Expected responses (intent: #{ intentName })" + }, + "export_as_skill_764cf284": { + "message": "Export as skill" + }, + "export_assets_to_zip_d2d798ab": { + "message": "Export assets to .zip" + }, + "export_de71cd8e": { + "message": "Export" + }, + "expression_7f906a13": { + "message": "Expression" + }, + "expression_starting_with_a750efc8": { + "message": "Expression starting with =." + }, + "expression_to_evaluate_ce4095b1": { + "message": "Expression to evaluate." + }, + "extension_name_e21a5e25": { + "message": "Extension name" + }, + "extensions_7250a0bb": { + "message": "Extensions" + }, + "external_resources_will_not_be_changed_c08b0009": { + "message": "External resources will not be changed." + }, + "extract_question_and_answer_pairs_from_an_online_f_7316548e": { + "message": "Extract question-and-answer pairs from an online FAQ, product manuals, or other files. Supported formats are .tsv, .pdf, .doc, .docx, .xlsx, containing questions and answers in sequence. Learn more about knowledge base sources. Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. Learn more about QnA Maker SKUs." + }, + "extract_question_and_answer_pairs_from_an_online_f_c1e12724": { + "message": "Extract question-and-answer pairs from an online FAQ, product manuals, or other files. Supported formats are .tsv, .pdf, .doc, .docx, .xlsx, containing questions and answers in sequence. " + }, + "extracting_qna_pairs_from_urls_eaed1b1c": { + "message": "Extracting QNA pairs from { urls }" + }, + "false_2f39ee6d": { + "message": "false" + }, + "false_eef8c169": { + "message": "False" + }, + "field_set_6e7d7f67": { + "message": "Field Set" + }, + "file_name_8fd421ff": { + "message": "File name" + }, + "file_not_found_9eec054c": { + "message": "File not found" + }, + "file_or_attachment_5467e604": { + "message": "File or attachment" + }, + "file_type_fd1ba7ee": { + "message": "File Type" + }, + "filter_dialog_de3ce43f": { + "message": "Filter Dialog" + }, + "firstselector_a3daca5d": { + "message": "FirstSelector" + }, + "folder_foldername_already_exists_4a2260e9": { + "message": "folder { folderName } already exists" + }, + "for_each_def04c48": { + "message": "For Each" + }, + "for_each_page_3b4d4b69": { + "message": "For Each Page" + }, + "form_editor_7c2b02f0": { + "message": "form editor" + }, + "form_title_baf85c7e": { + "message": "form title" + }, + "fromtemplatename_does_not_exist_d429483c": { + "message": "fromTemplateName does not exist" + }, + "gb_7570760e": { + "message": "GB" + }, + "general_24ac26a8": { + "message": "General" + }, + "generate_44e33e72": { + "message": "Generate" + }, + "get_a_new_copy_of_the_runtime_code_84970bf": { + "message": "Get a new copy of the runtime code" + }, + "get_started_50c13c6c": { + "message": "Get started!" + }, + "getting_help_ab6811b0": { + "message": "Getting Help" + }, + "getting_started_f45a7e87": { + "message": "Getting Started" + }, + "go_to_qna_all_up_view_page_d475333d": { + "message": "Go to QnA all-up view page." + }, + "got_it_2c06b54a": { + "message": "Got it!" + }, + "greeting_f906f962": { + "message": "Greeting" + }, + "handle_a_condition_f32eb8d": { + "message": "Handle a Condition" + }, + "handoff_activity_14363a20": { + "message": "Handoff activity" + }, + "handover_to_human_1a619574": { + "message": "Handover to human" + }, + "here_s_what_we_know_4e9c1731": { + "message": "Here’s what we know…" + }, + "hide_code_5dcffa94": { + "message": "Hide code" + }, + "home_351838cd": { + "message": "Home" + }, + "http_request_79847109": { + "message": "HTTP Request" + }, + "i_want_to_delete_this_bot_f81a4735": { + "message": "I want to delete this bot" + }, + "icon_name_is_file_c80dacae": { + "message": "{ icon } name is { file }" + }, + "iconname_file_icon_29976c8e": { + "message": "{ iconName } file icon" + }, + "id_already_exists_please_enter_a_unique_file_name_174913a3": { + "message": "{ id } already exists. Please enter a unique file name." + }, + "id_is_id_c0fc7a7d": { + "message": "id is { id }" + }, + "if_condition_56c9be4a": { + "message": "If Condition" + }, + "if_this_problem_persists_please_file_an_issue_on_6fbc8e2b": { + "message": "If this problem persists, please file an issue on" + }, + "if_you_already_have_a_luis_account_provide_the_inf_bede07a4": { + "message": "If you already have a LUIS account, provide the information below. If you do not have an account yet, create a (free) account first." + }, + "if_you_already_have_a_qna_account_provide_the_info_466d6a4b": { + "message": "If you already have a QNA account, provide the information below. If you do not have an account yet, create a (free) account first." + }, + "import_qna_from_url_26a99161": { + "message": "Import QnA From Url" + }, + "in_production_5a70b8b4": { + "message": "In production" + }, + "in_test_63c32694": { + "message": "In test" + }, + "in_the_b_create_a_trigger_b_wizard_set_the_trigger_f9b23519": { + "message": "In the Create a trigger wizard, set the trigger type to Activities in the dropdown. Then set the Activity Type to Greeting (ConversationUpdate activity), and click the Submit button." + }, + "increment_by_step_1cf3a88": { + "message": "increment by { step }" + }, + "input_1d1d9b8e": { + "message": "Input" + }, + "install_microsoft_net_core_sdk_2de509f0": { + "message": "Install Microsoft .NET Core SDK" + }, + "install_pre_release_versions_of_composer_daily_to__ceb41b54": { + "message": "Install pre-release versions of Composer, daily, to access and test the latest features. Learn more." + }, + "install_the_update_and_restart_composer_fac30a61": { + "message": "Install the update and restart Composer." + }, + "integer_b08abbe9": { + "message": "Integer" + }, + "integer_constant_650191ba": { + "message": "Integer constant." + }, + "integer_constant_or_expression_to_evaluate_ec4a17da": { + "message": "Integer constant or expression to evaluate." + }, + "integer_or_expression_107f60fb": { + "message": "Integer or expression" + }, + "intent_2291200b": { + "message": "Intent" + }, + "intent_recognized_c3840853": { + "message": "Intent recognized" + }, + "intentname_is_missing_or_empty_e49db2f8": { + "message": "intentName is missing or empty" + }, + "interpolated_string_c96053f2": { + "message": "Interpolated string." + }, + "interpolated_string_e04923d3": { + "message": "Interpolated string" + }, + "interpolated_string_or_expression_to_evaluate_ba0b0ba": { + "message": "Interpolated string or expression to evaluate." + }, + "introduction_of_key_concepts_and_user_experience_e_971b32e9": { + "message": "Introduction of key concepts and user experience elements for Composer." + }, + "invoke_activity_87df4903": { + "message": "Invoke activity" + }, + "is_missing_or_empty_a551462e": { + "message": "is missing or empty" + }, + "it_s_not_a_built_in_function_or_a_custom_function_1527ed83": { + "message": "it''s not a built-in function or a custom function." + }, + "item_actions_22d0242": { + "message": "Item Actions" + }, + "item_actions_cd903bde": { + "message": "Item actions" + }, + "item_added_f3910bed": { + "message": "item added" + }, + "jan_28_2020_8beb36dc": { + "message": "Jan 28, 2020" + }, + "kb_d9c53902": { + "message": "KB" + }, + "key_cannot_be_blank_dccc1b46": { + "message": "Key cannot be blank" + }, + "key_f2a919d5": { + "message": "Key" + }, + "keys_must_be_unique_2028cef3": { + "message": "Keys must be unique" + }, + "l_startline_startcharacter_l_endline_endcharacter_72bc2e5d": { + "message": "L{ startLine }:{ startCharacter } - L{ endLine }:{ endCharacter } " + }, + "language_generation_1876f6d6": { + "message": "Language Generation" + }, + "language_generation_file_80c08efe": { + "message": "language generation file" + }, + "language_understanding_9ae3f1f6": { + "message": "Language Understanding" + }, + "language_understanding_file_a65abc2c": { + "message": "language understanding file" + }, + "languagepolicy_e754ad28": { + "message": "LanguagePolicy" + }, + "last_modified_time_is_time_b1497d3": { + "message": "Last modified time is { time }" + }, + "learn_more_a79a7918": { + "message": "Learn more" + }, + "learn_more_about_knowledge_base_sources_24369b09": { + "message": "Learn more about knowledge base sources. " + }, + "learn_more_about_qna_maker_skus_998c567": { + "message": "Learn more about QnA Maker SKUs." + }, + "learn_more_about_title_d1d3edbe": { + "message": "Learn more about { title }" + }, + "learn_more_c08939e8": { + "message": "Learn more." + }, + "learn_the_basics_2d9ae7df": { + "message": "Learn the basics" + }, + "leave_product_tour_49585718": { + "message": "Leave Product Tour?" + }, + "lg_editor_ee0184e6": { + "message": "LG editor" + }, + "lg_file_already_exist_55195d20": { + "message": "lg file already exist" + }, + "lg_file_id_not_found_6bd6869b": { + "message": "LG file { id } not found" + }, + "lg_language_client_7892c4c1": { + "message": "LG Language Client" + }, + "lg_navigation_pane_97b15c3d": { + "message": "LG Navigation Pane" + }, + "line_numbers_1bfa7fb": { + "message": "Line numbers" + }, + "line_startline_startcharacter_line_endline_endchar_372bb2f4": { + "message": "line { startLine }:{ startCharacter } - line { endLine }:{ endCharacter }" + }, + "link_to_where_this_luis_intent_is_defined_9be25fb7": { + "message": "link to where this LUIS intent is defined" + }, + "list_view_e33843f0": { + "message": "List view" + }, + "loading_25990131": { + "message": "Loading..." + }, + "loading_bde52856": { + "message": "Loading" + }, + "location_e17bd402": { + "message": "Location" + }, + "location_is_location_8c17b5de": { + "message": "location is { location }" + }, + "log_to_console_4fc23e34": { + "message": "Log to console" + }, + "login_6f3d6249": { + "message": "Login" + }, + "login_to_composer_eb42b41": { + "message": "Login to Composer" + }, + "loop_for_each_item_53eb7c5b": { + "message": "Loop: for each item" + }, + "loop_for_each_item_e09537ae": { + "message": "Loop: For each item" + }, + "loop_for_each_page_multiple_items_733e7f41": { + "message": "Loop: For each page (multiple items)" + }, + "looping_ddae56ff": { + "message": "Looping" + }, + "lu_editor_d09fb2b0": { + "message": "LU editor" + }, + "lu_file_id_not_found_8732d33e": { + "message": "LU file { id } not found" + }, + "lu_language_client_bbffcd7b": { + "message": "LU Language Client" + }, + "lu_navigation_pane_54dc5587": { + "message": "LU Navigation Pane" + }, + "luis_add4bbe3": { + "message": "LUIS" + }, + "luis_authoring_key_cfaba7dd": { + "message": "LUIS Authoring key:" + }, + "luis_authoring_region_b142f97b": { + "message": "Luis Authoring Region" + }, + "main_dialog_eed5c847": { + "message": "Main dialog" + }, + "make_a_copy_77d1233": { + "message": "Make a copy" + }, + "manage_properties_c9fa4456": { + "message": "Manage properties" + }, + "manifest_url_30824e88": { + "message": "Manifest url" + }, + "manifest_url_can_not_be_accessed_a7f147b2": { + "message": "Manifest url can not be accessed" + }, + "manifest_version_1edc004a": { + "message": "Manifest Version" + }, + "mb_8f9f9e84": { + "message": "MB" + }, + "message_5c38209d": { + "message": "Message" + }, + "message_deleted_628eef3a": { + "message": "Message deleted" + }, + "message_deleted_activity_85dd8915": { + "message": "Message deleted activity" + }, + "message_reaction_3704d790": { + "message": "Message reaction" + }, + "message_reaction_activity_379944e7": { + "message": "Message reaction activity" + }, + "message_received_5abfe9a0": { + "message": "Message received" + }, + "message_received_activity_1ff7c2a4": { + "message": "Message received activity" + }, + "message_updated_4f2e37fe": { + "message": "Message updated" + }, + "message_updated_activity_4997737e": { + "message": "Message updated activity" + }, + "microsoft_app_id_a7f3e591": { + "message": "Microsoft App Id" + }, + "microsoft_app_password_737ebc90": { + "message": "Microsoft App Password" + }, + "minimap_beb3be27": { + "message": "Minimap" + }, + "missing_definition_for_defname_33f2b594": { + "message": "Missing definition for { defName }" + }, + "missing_fields_1c88ab71": { + "message": "missing fields" + }, + "modification_rejected_6a6e8322": { + "message": "Modification Rejected" + }, + "modify_active_dialog_edcf4a45": { + "message": "Modify active dialog" + }, + "modify_this_dialog_3c38af24": { + "message": "Modify this dialog" + }, + "more_options_e89670cf": { + "message": "More options" + }, + "mostspecificselector_2cf1a6ae": { + "message": "MostSpecificSelector" + }, + "move_abf00365": { + "message": "Move" + }, + "move_down_eaae3426": { + "message": "Move Down" + }, + "move_up_b1c4d3a5": { + "message": "Move Up" + }, + "msft_ignite_ai_show_e131edef": { + "message": "MSFT Ignite AI Show" + }, + "multi_choice_839b54bb": { + "message": "Multi-choice" + }, + "multiple_wildcards_26f50b6c": { + "message": "multiple wildcards" + }, + "must_be_an_expression_error_477cbda6": { + "message": "must be an expression: { error }" + }, + "must_have_a_name_d5c5c464": { + "message": "Must have a name" + }, + "my_publish_profile_2ed56828": { + "message": "My Publish Profile" + }, + "name_1aed4a1b": { + "message": "Name" + }, + "name_and_save_your_skill_manifest_cfd672b7": { + "message": "Name and save your skill manifest." + }, + "name_cannot_include_special_characters_or_spaces_59a1950b": { + "message": "Name cannot include special characters or spaces" + }, + "name_copy_55d27c1a": { + "message": "{ name }_Copy" + }, + "name_is_name_abc564f9": { + "message": "Name is { name }" + }, + "name_is_required_29d28f8d": { + "message": "{ name } is required" + }, + "name_of_skill_dialog_to_call_201707f3": { + "message": "Name of skill dialog to call" + }, + "navigation_control_94f2649e": { + "message": "navigation control" + }, + "navigation_pane_e587b73c": { + "message": "Navigation pane" + }, + "navigation_panel_530ae4ac": { + "message": "Navigation panel" + }, + "navigation_path_8b299e64": { + "message": "Navigation Path" + }, + "navigation_to_see_actions_3be545c9": { + "message": "navigation to see actions" + }, + "new_13daf639": { + "message": "New" + }, + "new_template_49e6f0f2": { + "message": "New template" + }, + "new_trigger_331c811b": { + "message": "New Trigger .." + }, + "new_update_available_30534ea5": { + "message": "New update available" + }, + "new_value_3c1ce474": { + "message": "New value" + }, + "newsectionplaceholdersectionname_5fc0d21": { + "message": "_NewSectionPlaceHolderSectionName" + }, + "next_40e12421": { + "message": "Next" + }, + "no_editor_for_type_8b5593c5": { + "message": "No Editor for { type }" + }, + "no_lu_file_with_name_id_fb21315d": { + "message": "NO LU FILE WITH NAME { id }" + }, + "no_lu_or_qna_file_with_name_id_21cfe9dc": { + "message": "NO LU OR QNA FILE WITH NAME { id }" + }, + "no_updates_available_cecd904d": { + "message": "No updates available" + }, + "no_wildcard_ff439e76": { + "message": "no wildcard" + }, + "node_menu_e2aa8092": { + "message": "Node menu" + }, + "not_a_single_template_e37f894": { + "message": "Not a single template" + }, + "not_yet_published_669e37b3": { + "message": "Not yet published" + }, + "notification_list_ee2abb6c": { + "message": "Notification list" + }, + "notification_message_msg_7d55d08e": { + "message": "Notification Message { msg }" + }, + "notification_type_263c145d": { + "message": "Notification type" + }, + "notifications_cbfa7704": { + "message": "Notifications" + }, + "nov_12_2019_96ec5473": { + "message": "Nov 12, 2019" + }, + "number_a6dc44e": { + "message": "Number" + }, + "number_constant_1938e96e": { + "message": "Number constant." + }, + "number_constant_or_expression_to_evaluate_1098771": { + "message": "Number constant or expression to evaluate." + }, + "number_or_expression_55c7f9f": { + "message": "Number or expression" + }, + "numeric_field_c2564f69": { + "message": "numeric field" + }, + "oauth_login_b6aa9534": { + "message": "OAuth login" + }, + "object_345070f6": { + "message": "Object" + }, + "object_constant_77052b91": { + "message": "Object constant." + }, + "object_or_expression_89903fa1": { + "message": "Object or expression" + }, + "object_or_expression_to_evaluate_699c8827": { + "message": "Object or expression to evaluate." + }, + "off_5385ef3f": { + "message": "Off" + }, + "ok_c47544a2": { + "message": "OK" + }, + "ok_ff1b646a": { + "message": "Ok" + }, + "okay_1772bbeb": { + "message": "Okay" + }, + "on_8f7190e5": { + "message": "On" + }, + "onboarding_8407871c": { + "message": "Onboarding" + }, + "ondialogevents_types_3dc569b5": { + "message": "OnDialogEvents Types" + }, + "open_e0beb7b9": { + "message": "Open" + }, + "open_inline_editor_a5aabcfa": { + "message": "Open inline editor" + }, + "or_4f7d4edb": { + "message": "Or: " + }, + "origin_lg_file_not_found_in_store_d194cdbc": { + "message": "origin lg file not found in store" + }, + "origin_lu_file_not_found_in_store_19e5cc8f": { + "message": "origin lu file not found in store" + }, + "original_ce7b7e64": { + "message": " - Original" + }, + "other_1c6d9c79": { + "message": "Other" + }, + "output_5023cf84": { + "message": "Output" + }, + "page_number_cdee4179": { + "message": "Page number" + }, + "parse_failed_at_name_error_8f562bda": { + "message": "Parse failed at { name }: { error }" + }, + "paste_5963d1c1": { + "message": "Paste" + }, + "please_add_at_least_minitems_endpoint_5439fd74": { + "message": "Please add at least { minItems } endpoint" + }, + "please_enter_a_value_for_key_77cfc097": { + "message": "Please enter a value for { key }" + }, + "please_enter_an_event_name_a148275a": { + "message": "Please enter an event name" + }, + "please_input_a_manifest_url_d726edbf": { + "message": "Please input a manifest Url" + }, + "please_input_regex_pattern_5cd659a2": { + "message": "Please input regEx pattern" + }, + "please_log_in_before_continuing_b6eace13": { + "message": "Please log in before continuing." + }, + "please_return_to_design_view_to_start_the_onboardi_a561af9d": { + "message": "Please return to Design View to start the Onboarding tutorial." + }, + "please_select_a_event_type_200c4c9f": { + "message": "Please select a event type" + }, + "please_select_a_specific_qna_file_to_import_qna_d71d871": { + "message": "Please select a specific qna file to import QnA" + }, + "please_select_a_trigger_type_67417abb": { + "message": "Please select a trigger type" + }, + "please_select_a_valid_endpoint_bf608af1": { + "message": "Please select a valid endpoint" + }, + "please_select_a_version_of_the_manifest_schema_4a3efbb1": { + "message": "Please select a version of the manifest schema" + }, + "please_select_an_activity_type_92f4a8a1": { + "message": "Please select an activity type" + }, + "populate_your_knowledge_base_bb2d3605": { + "message": "Populate your Knowledge Base" + }, + "press_enter_to_add_this_item_or_tab_to_move_to_the_6beb8a14": { + "message": "press Enter to add this item or Tab to move to the next interactive element" + }, + "press_enter_to_add_this_name_and_advance_to_the_ne_6a2ae080": { + "message": "press Enter to add this name and advance to the next row, or press Tab to advance to the value field" + }, + "previous_bd2ac015": { + "message": "Previous" + }, + "previous_folder_e7eeb306": { + "message": "previous folder" + }, + "privacy_290109ea": { + "message": "Privacy" + }, + "privacy_button_b58e437": { + "message": "Privacy button" + }, + "progress_of_total_87de8616": { + "message": "{ progress }% of { total }" + }, + "prompt_for_a_date_5d2c689e": { + "message": "Prompt for a date" + }, + "prompt_for_a_date_or_a_time_d2df7f90": { + "message": "Prompt for a date or a time" + }, + "prompt_for_a_file_or_an_attachment_1bf18e7e": { + "message": "Prompt for a file or an attachment" + }, + "prompt_for_a_number_84999edb": { + "message": "Prompt for a number" + }, + "prompt_for_attachment_727d4fac": { + "message": "Prompt for Attachment" + }, + "prompt_for_confirmation_dc85565c": { + "message": "Prompt for confirmation" + }, + "prompt_for_text_5c524f80": { + "message": "Prompt for text" + }, + "prompt_with_multi_choice_f428542f": { + "message": "Prompt with multi-choice" + }, + "property_description_8d21ea2e": { + "message": "Property description." + }, + "property_editor_preferences_ad7a4b62": { + "message": "Property editor preferences" + }, + "property_title_f2b443b7": { + "message": "Property title" + }, + "property_type_95689fa5": { + "message": "Property type." + }, + "provide_a_brief_description_it_will_appear_on_the__f962eb38": { + "message": "Provide a brief description. It will appear on the publish history list." + }, + "pseudo_1a319287": { + "message": "Pseudo" + }, + "publish_5211dca3": { + "message": "Publish" + }, + "publish_configuration_d759a4e3": { + "message": "Publish Configuration" + }, + "publish_destination_type_5f8c200a": { + "message": "Publish Destination Type" + }, + "publish_models_9a36752a": { + "message": "Publish models" + }, + "publish_profiles_cf8d307b": { + "message": "Publish Profiles" + }, + "publish_to_selected_profile_c0998a2": { + "message": "Publish to selected profile" + }, + "published_4bb5209e": { + "message": "Published" + }, + "publishing_d63a8f2d": { + "message": "Publishing" + }, + "qna_28ee5e26": { + "message": "QnA" + }, + "qna_editor_9eb94b02": { + "message": "QnA editor" + }, + "qna_file_37705cb7": { + "message": "qna file" + }, + "qna_intent_recognized_49c3d797": { + "message": "QnA Intent recognized" + }, + "qna_navigation_pane_b79ebcbf": { + "message": "Qna Navigation Pane" + }, + "qna_region_5a864ef8": { + "message": "QnA Region" + }, + "qna_subscription_key_ed72a47": { + "message": "QNA Subscription key:" + }, + "question_9121487": { + "message": "Question" + }, + "randomselector_4a5274f1": { + "message": "RandomSelector" + }, + "range_selection_30caeea5": { + "message": "Range Selection" + }, + "re_prompt_for_input_c3b5b3ab": { + "message": "Re-prompt for input" + }, + "recent_bots_53585911": { + "message": "Recent Bots" + }, + "recognizer_type_dc591e16": { + "message": "Recognizer Type" + }, + "recognizers_cefce9d1": { + "message": "Recognizers" + }, + "redo_363c58b7": { + "message": "Redo" + }, + "redo_is_not_supported_b743e4dc": { + "message": "Redo is not supported" + }, + "refer_to_the_syntax_documentation_here_df8dc9b4": { + "message": "Refer to the syntax documentation here." + }, + "regex_intent_is_already_defined_df095c1f": { + "message": "RegEx { intent } is already defined" + }, + "regular_expression_855557bf": { + "message": "Regular Expression" + }, + "release_1af20f26": { + "message": "Release: " + }, + "reloading_49d2f661": { + "message": "Reloading" + }, + "remove_f47dc62a": { + "message": "Remove" + }, + "repeat_this_dialog_83ca994e": { + "message": "Repeat this dialog" + }, + "replace_this_dialog_e304015e": { + "message": "Replace this dialog" + }, + "reprompt_dialog_event_c42d2c33": { + "message": "Reprompt dialog event" + }, + "response_is_response_3cd62f8f": { + "message": "Response is { response }" + }, + "responses_12d6df1d": { + "message": "Responses" + }, + "restart_bot_34e36428": { + "message": "Restart Bot" + }, + "review_and_generate_63dec712": { + "message": "Review and generate" + }, + "rollback_26326307": { + "message": "Rollback" + }, + "runtime_already_exists_f181f5ec": { + "message": "Runtime already exists" + }, + "runtime_code_location_4e94ee12": { + "message": "Runtime code location" + }, + "runtime_config_a2904ff9": { + "message": "Runtime Config" + }, + "sample_phrases_5d78fa35": { + "message": "Sample Phrases" + }, + "sample_phrases_are_phrases_a966b47f": { + "message": "Sample Phrases are { phrases }" + }, + "save_11a80ec3": { + "message": "Save" + }, + "save_as_9e0cf70b": { + "message": "Save as" + }, + "save_your_skill_manifest_63bf5f26": { + "message": "Save your skill manifest" + }, + "schema_24739a48": { + "message": "Schema" + }, + "scripts_successfully_updated_3a75d57f": { + "message": "Scripts successfully updated." + }, + "sdk_runtime_packages_22d8ed1a": { + "message": "SDK runtime packages" + }, + "search_280d00bd": { + "message": "Search" + }, + "search_for_extensions_a27c4772": { + "message": "Search for extensions" + }, + "see_log_4b833bf7": { + "message": "See Log" + }, + "select_a_bot_e1c4dc2b": { + "message": "Select a Bot" + }, + "select_a_trigger_on_the_left_a4b41558": { + "message": "Select a trigger on the left" + }, + "select_all_f73344a8": { + "message": "Select all" + }, + "select_an_activity_type_c982fcbe": { + "message": "Select an activity type" + }, + "select_an_event_type_3d7108f1": { + "message": "Select an event type" + }, + "select_language_to_delete_d1662d3d": { + "message": "Select language to delete" + }, + "select_manifest_version_4f5b1230": { + "message": "Select manifest version" + }, + "select_options_9ee7b227": { + "message": "Select options" + }, + "select_property_type_5f3ab685": { + "message": "select property type" + }, + "select_runtime_version_to_add_d63d383b": { + "message": "Select runtime version to add" + }, + "select_the_language_that_bot_will_be_able_to_under_1f2bcb96": { + "message": "Select the language that bot will be able to understand (User input) and respond to (Bot responses).\n To make this bot available in other languages, click “Add’ to create a copy of the default language, and translate the content into the new language." + }, + "select_which_dialogs_are_included_in_the_skill_man_281ef8c9": { + "message": "Select which dialogs are included in the skill manifest" + }, + "select_which_tasks_this_skill_can_perform_172b0eae": { + "message": "Select which tasks this skill can perform" + }, + "selection_field_86d1dc94": { + "message": "selection field" + }, + "selectors_2dcb3029": { + "message": "Selectors" + }, + "send_a_response_1a917d7e": { + "message": "Send a response" + }, + "send_an_http_request_aa32fd2": { + "message": "Send an HTTP request" + }, + "send_messages_c48b239": { + "message": "Send Messages" + }, + "sentence_wrap_930c8ced": { + "message": "Sentence wrap" + }, + "session_expired_12aaf414": { + "message": "Session expired" + }, + "set_a_property_4258d8d7": { + "message": "Set a property" + }, + "set_destination_folder_f76e0259": { + "message": "Set destination folder" + }, + "set_properties_7415af3c": { + "message": "Set properties" + }, + "set_up_your_bot_75009578": { + "message": "Set up your bot" + }, + "settings_5aa0fd0c": { + "message": "Settings" + }, + "settings_contains_detailed_information_about_your__54aa601d": { + "message": "Settings contains detailed information about your bot. For security reasons, they are hidden by default. To test your bot or publish to Azure, you may need to provide these settings." + }, + "settings_editor_b5246933": { + "message": "Settings editor" + }, + "settings_menu_c99ecc6d": { + "message": "Settings menu" + }, + "show_all_notifications_55bf7858": { + "message": "Show All Notifications" + }, + "show_code_f3e9d1cc": { + "message": "Show code" + }, + "show_keys_3072a5b8": { + "message": "Show keys" + }, + "show_skill_manifest_5d0abde1": { + "message": "Show skill manifest" + }, + "sign_out_user_6845d640": { + "message": "Sign out user" + }, + "skill_dialog_name_1bbf0eff": { + "message": "Skill Dialog Name" + }, + "skill_endpoint_b563491e": { + "message": "Skill Endpoint" + }, + "skill_endpoints_e4e3d8c1": { + "message": "Skill endpoints" + }, + "skill_host_endpoint_b1088d0": { + "message": "Skill Host Endpoint" + }, + "skill_manifest_endpoint_is_configured_improperly_e083731d": { + "message": "Skill manifest endpoint is configured improperly" + }, + "skillname_manifest_ef3d9fed": { + "message": "{ skillName } Manifest" + }, + "skills_49cccd6a": { + "message": "Skills" + }, + "skip_this_step_to_add_questions_and_answers_manual_ed1b9f80": { + "message": "Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. " + }, + "sorry_something_went_wrong_with_connecting_bot_run_7d6785e3": { + "message": "Sorry, something went wrong with connecting bot runtime" + }, + "sorry_something_went_wrong_with_publishing_try_aga_306a48f5": { + "message": "Sorry, something went wrong with publishing. Try again or exit out of this task." + }, + "sorted_a_to_z_915b2ed3": { + "message": "Sorted A to Z" + }, + "sorted_z_to_a_722f1567": { + "message": "Sorted Z to A" + }, + "spaces_and_special_characters_are_not_allowed_20d47684": { + "message": "Spaces and special characters are not allowed." + }, + "spaces_and_special_characters_are_not_allowed_use__48acec3c": { + "message": "Spaces and special characters are not allowed. Use letters, numbers, -, or _." + }, + "specify_a_name_and_description_for_your_new_dialog_86eb3130": { + "message": "Specify a name and description for your new dialog." + }, + "specify_a_name_description_and_location_for_your_n_667f1438": { + "message": "Specify a name, description, and location for your new bot project." + }, + "start_bot_25ecad14": { + "message": "Start Bot" + }, + "start_bot_failed_d75647d5": { + "message": "Start bot failed" + }, + "start_command_a085f2ec": { + "message": "Start command" + }, + "state_is_state_a2b8943": { + "message": "State is { state }" + }, + "status_e7fdbe06": { + "message": "Status" + }, + "step_of_setlength_43c73821": { + "message": "{ step } of { setLength }" + }, + "string_24369b3": { + "message": "String" + }, + "string_field_db491570": { + "message": "string field" + }, + "string_or_expression_c441b45c": { + "message": "String or expression" + }, + "submit_a3cc6859": { + "message": "Submit" + }, + "synonyms_optional_afe5cdb1": { + "message": "Synonyms (Optional)" + }, + "tb_149f379c": { + "message": "TB" + }, + "templatename_is_missing_or_empty_23e6b06e": { + "message": "templateName is missing or empty" + }, + "terms_of_use_6542769b": { + "message": "Terms of Use" + }, + "test_in_emulator_b1b3c278": { + "message": "Test in Emulator" + }, + "test_your_bot_3cd1f4bb": { + "message": "Test your bot" + }, + "text_7f4593da": { + "message": "Text" + }, + "the_callback_url_for_the_skill_host_e20e1012": { + "message": "The callback url for the skill host." + }, + "the_dialog_you_have_tried_to_delete_is_currently_u_a37c7a02": { + "message": "The dialog you have tried to delete is currently used in the below dialog(s). Removing this dialog will cause your Bot to malfunction without additional action." + }, + "the_following_lufile_s_are_invalid_c61ea748": { + "message": "The Following LuFile(s) are invalid: \n" + }, + "the_main_dialog_is_named_after_your_bot_it_is_the__3d9864f": { + "message": "The main dialog is named after your bot. It is the root and entry point of a bot." + }, + "the_manifest_can_be_edited_and_refined_manually_if_9269e3f2": { + "message": "The manifest can be edited and refined manually if and where needed." + }, + "the_microsoft_app_id_that_will_be_calling_the_skil_955424de": { + "message": "The Microsoft App Id that will be calling the skill." + }, + "the_microsoft_app_password_that_will_be_calling_th_acf9e1ea": { + "message": "The Microsoft App Password that will be calling the skill." + }, + "the_page_you_are_looking_for_can_t_be_found_a3b75795": { + "message": "The page you are looking for can''t be found." + }, + "the_return_type_does_not_match_2ae72548": { + "message": "the return type does not match" + }, + "the_welcome_message_is_triggered_by_the_i_conversa_a3ff58f8": { + "message": "The welcome message is triggered by the ConversationUpdate event. To add a new ConversationUpdate trigger:" + }, + "there_was_an_error_74ed3c58": { + "message": "There was an error" + }, + "there_was_error_creating_your_kb_53b31ff3": { + "message": "There was error creating your KB" + }, + "these_examples_bring_together_all_of_the_best_prac_ca1b89c7": { + "message": "These examples bring together all of the best practices and supporting components we''ve identified through building of conversational experiences." + }, + "these_tasks_will_be_used_to_generate_the_manifest__2791be0e": { + "message": "These tasks will be used to generate the manifest and describe the capabilities of this skill to those who may want to use it." + }, + "this_configures_a_data_driven_dialog_via_a_collect_c7fa4389": { + "message": "This configures a data driven dialog via a collection of events and actions." + }, + "this_dialog_has_no_trigger_yet_d1f1d173": { + "message": "This dialog has no trigger yet." + }, + "this_is_a_required_field_acb9837e": { + "message": "This is a required field." + }, + "this_is_a_severity_notification_9beabb58": { + "message": "This is a { severity } notification" + }, + "this_is_the_language_used_for_composer_s_user_inte_ab7fa82e": { + "message": "This is the language used for Composer’s user interface." + }, + "this_language_will_be_copied_and_used_as_the_basis_573515e4": { + "message": "This language will be copied and used as the basis (and fallback language) for the translation." + }, + "this_trigger_type_is_not_supported_by_the_regex_re_2c3b7c46": { + "message": "This trigger type is not supported by the RegEx recognizer and will not be fired." + }, + "this_trigger_type_is_not_supported_by_the_regex_re_dc3eefa2": { + "message": "This trigger type is not supported by the RegEx recognizer. To ensure this trigger is fired, change the recognizer type." + }, + "this_url_is_duplicated_a0768f44": { + "message": "This url is duplicated" + }, + "this_version_of_the_content_is_out_of_date_and_you_5e878f29": { + "message": "This version of the content is out of date, and your last change was rejected. The content will be automatically refreshed." + }, + "this_will_delete_the_dialog_and_its_contents_do_yo_9b48fa3c": { + "message": "This will delete the Dialog and its contents. Do you wish to continue?" + }, + "this_will_delete_the_profile_do_you_wish_to_contin_61b54894": { + "message": "This will delete the profile. Do you wish to continue?" + }, + "this_will_open_your_emulator_application_if_you_do_ba277151": { + "message": "This will open your Emulator application. If you don''t yet have the Bot Framework Emulator installed, you can download it here." + }, + "time_2b5aac58": { + "message": "Time" + }, + "tip_8f74cd0": { + "message": "tip" + }, + "tips_80d0da2b": { + "message": "tips" + }, + "title_ee03d132": { + "message": "Title" + }, + "title_msg_ee91458d": { + "message": "{ title }. { msg }" + }, + "to_customize_the_welcome_message_select_the_i_send_9b4bf4f": { + "message": "To customize the welcome message, select the Send a response action in the Visual Editor. Then in the Form Editor on the right, you can edit the bot''s welcome message in the Language Generation field." + }, + "to_learn_more_about_the_lg_file_format_read_the_do_ef6e083d": { + "message": "> To learn more about the LG file format, read the documentation at\n> { lgHelp }" + }, + "to_learn_more_about_the_lu_file_format_read_the_do_cac5ffc9": { + "message": "> To learn more about the LU file format, read the documentation at\n> { LU_HELP }" + }, + "to_learn_more_about_the_qna_file_format_read_the_d_1ce18259": { + "message": "> To learn more about the QnA file format, read the documentation at\n> { QNA_HELP }" + }, + "to_make_your_bot_available_for_others_as_a_skill_w_f2c19b9c": { + "message": "To make your bot available for others as a skill, we need to generate a manifest." + }, + "to_run_this_bot_composer_needs_net_core_sdk_d1551038": { + "message": "To run this bot, Composer needs .NET Core SDK." + }, + "to_understand_what_the_user_says_your_dialog_needs_4e791611": { + "message": "To understand what the user says, your dialog needs a \"Recognizer\"; that includes example words and sentences that users may use." + }, + "to_understand_what_the_user_says_your_dialog_needs_957034cc": { + "message": "To understand what the user says, your dialog needs an ''IRecognizer'' that includes example words and sentences that users may use." + }, + "to_which_language_will_you_be_translating_your_bot_77219d69": { + "message": "To which language will you be translating your bot?" + }, + "toolbar_bafd4228": { + "message": "toolbar" + }, + "total_mb_531a3721": { + "message": "{ total }MB" + }, + "trigger_phrases_f6754fa": { + "message": "Trigger phrases" + }, + "trigger_phrases_intent_intentname_a1b62148": { + "message": "Trigger phrases (intent: #{ intentName })" + }, + "triggers_connect_intents_with_bot_responses_think__fdfc97ea": { + "message": "Triggers connect intents with bot responses. Think of a trigger as one capability of your bot. So your bot is a collection of triggers. To add a new trigger, click the Add button in the toolbar, and then select the Add a new trigger option from the dropdown menu." + }, + "true_1900d7ae": { + "message": "true" + }, + "true_b9327890": { + "message": "True" + }, + "trueselector_40702dda": { + "message": "TrueSelector" + }, + "try_again_ad656c3c": { + "message": "Try again" + }, + "type_c8106334": { + "message": "Type" + }, + "type_could_not_be_loaded_65ebaf86": { + "message": "{ type } could not be loaded" + }, + "type_dialog_name_14a50769": { + "message": "Type dialog name" + }, + "typing_activity_6b634ae": { + "message": "Typing activity" + }, + "unable_to_determine_recognizer_type_from_data_valu_2960f526": { + "message": "Unable to determine recognizer type from data: { value }" + }, + "undo_a7be8fef": { + "message": "Undo" + }, + "undo_is_not_supported_ecd6f9fc": { + "message": "Undo is not supported" + }, + "unknown_intent_44b962ba": { + "message": "Unknown intent" + }, + "unknown_intent_recognized_1953f9be": { + "message": "Unknown intent recognized" + }, + "unknown_state_23f73afb": { + "message": "Unknown State" + }, + "unused_8d193e3": { + "message": "Unused" + }, + "update_a_an_activity_previously_sent_during_the_co_f0619cca": { + "message": "Update a an activity previously sent during the conversation" + }, + "update_activity_2b05e6c6": { + "message": "Update activity" + }, + "update_available_b637d767": { + "message": "Update available" + }, + "update_complete_c5163fbf": { + "message": "Update complete" + }, + "update_failed_2c87428c": { + "message": "Update failed" + }, + "update_folder_name_error_24563bf6": { + "message": "Update Folder Name Error" + }, + "update_in_progress_f65e6b29": { + "message": "Update in progress" + }, + "update_scripts_a3a483e": { + "message": "Update scripts" + }, + "update_scripts_c58771a2": { + "message": "Update Scripts" + }, + "updating_scripts_e17a5722": { + "message": "Updating scripts... " + }, + "url_22a5f3b8": { + "message": "URL" + }, + "url_8c4ff7d2": { + "message": "Url" + }, + "url_should_start_with_http_s_9ca55d94": { + "message": "Url should start with http[s]://" + }, + "use_custom_runtime_d7d323fd": { + "message": "Use custom runtime" + }, + "used_3d895705": { + "message": "Used" + }, + "used_in_126529e5": { + "message": "Used In" + }, + "user_input_673e4a89": { + "message": "User input" + }, + "user_input_a6ff658d": { + "message": "User Input" + }, + "user_is_typing_790cb502": { + "message": "User is typing" + }, + "validating_35b79a96": { + "message": "Validating..." + }, + "validation_rules_efd3144d": { + "message": "Validation Rules" + }, + "value_d842f16d": { + "message": "Value" + }, + "version_5599c321": { + "message": "Version" + }, + "version_version_a051e218": { + "message": "Version { version }" + }, + "video_tutorials_79eb26ca": { + "message": "Video tutorials:" + }, + "view_ba339f93": { + "message": "View" + }, + "view_kb_c382e495": { + "message": "View KB" + }, + "vishwac_sena_45910bf0": { + "message": "Vishwac Sena" + }, + "visual_editor_216472d": { + "message": "Visual editor" + }, + "warning_53c98b03": { + "message": "Warning!" + }, + "warning_the_action_you_are_about_to_take_cannot_be_1071a3c3": { + "message": "Warning: the action you are about to take cannot be undone. Going further will delete this bot and any related files in the bot project folder." + }, + "we_have_created_a_sample_bot_to_help_you_get_start_95a58922": { + "message": "We have created a sample bot to help you get started with Composer. Click here to open the bot." + }, + "we_need_to_define_the_endpoints_for_the_skill_to_a_5dc98d90": { + "message": "We need to define the endpoints for the skill to allow other bots to interact with it." + }, + "weather_bot_c38920cd": { + "message": "Weather Bot" + }, + "welcome_73d18b4d": { + "message": "Welcome!" + }, + "welcome_dd4e7151": { + "message": "Welcome" + }, + "westeurope_cabf9688": { + "message": "westeurope" + }, + "westus_dc50d800": { + "message": "westus" + }, + "what_can_the_user_accomplish_through_this_conversa_7ddb03a1": { + "message": "What can the user accomplish through this conversation? For example, BookATable, OrderACoffee etc." + }, + "what_is_the_name_of_the_custom_event_b28a7b3": { + "message": "What is the name of the custom event?" + }, + "what_is_the_name_of_this_trigger_2642266e": { + "message": "What is the name of this trigger" + }, + "what_is_the_name_of_this_trigger_luis_17b60a23": { + "message": "What is the name of this trigger (LUIS)" + }, + "what_is_the_name_of_this_trigger_regex_f77376d7": { + "message": "What is the name of this trigger (RegEx)" + }, + "what_is_the_name_of_your_bot_a571c565": { + "message": "What is the name of your bot?" + }, + "what_is_the_type_of_this_trigger_d2701744": { + "message": "What is the type of this trigger?" + }, + "what_your_bot_says_to_the_user_this_is_a_template__a8d2266d": { + "message": "What your bot says to the user. This is a template used to create the outgoing message. It can include language generation rules, properties from memory, and other features.\n\nFor example, to define variations that will be chosen at random, write:\n- hello\n- hi" + }, + "what_your_bot_says_to_the_user_visit_a_target_blan_7735479": { + "message": "What your Bot says to the user. Visit the documentation a reference of capabilities." + }, + "when_deleting_a_language_only_the_content_will_be__8f7f8dee": { + "message": "When deleting a language, only the content will be removed. The flow and logic of the conversation and dialog will remain functional." + }, + "when_done_switch_to_the_newly_created_language_and_862b7dd9": { + "message": "When done, switch to the newly created language and start the (manual) translation process." + }, + "when_multiple_people_are_working_with_models_you_w_32b48099": { + "message": "When multiple people are working with models you want to be able to work with models independently from each other tied to the source control." + }, + "which_activity_type_18333457": { + "message": "Which activity type?" + }, + "which_bot_do_you_want_to_open_974bb1e5": { + "message": "Which bot do you want to open?" + }, + "which_event_6e655d2b": { + "message": "Which event?" + }, + "yes_dde87d5": { + "message": "Yes" + }, + "you_are_about_to_publish_your_bot_to_the_profile_b_79a6a226": { + "message": "You are about to publish your bot to the profile below. Do you want to proceed?" + }, + "you_can_create_a_new_bot_from_scratch_with_compose_1486288c": { + "message": "You can create a new bot from scratch with Composer, or start with a template." + }, + "you_can_define_and_manage_b_intents_b_here_each_in_721b8a0c": { + "message": "You can define and manage intents here. Each intent describes a particular user intention through utterances (i.e. user says). Intents are often triggers of your bot." + }, + "you_can_manage_all_bot_responses_here_make_good_us_5e6e1953": { + "message": "You can manage all bot responses here. Make good use of the templates to create sophisticated response logic based on your own needs." + }, + "you_do_not_have_permission_to_save_bots_here_56cc10c7": { + "message": "You do not have permission to save bots here" + }, + "your_bot_creation_journey_on_composer_131c1a8b": { + "message": "Your bot creation journey on Composer" + }, + "your_bot_is_using_luis_and_qna_for_natural_languag_53830684": { + "message": "Your bot is using LUIS and QNA for natural language understanding." + }, + "your_knowledge_base_is_ready_6ecc1871": { + "message": "Your knowledge base is ready!" + } +} \ No newline at end of file diff --git a/Composer/packages/ui-plugins/composer/package.json b/Composer/packages/ui-plugins/composer/package.json index 6ca898232d..c59aa6fc15 100644 --- a/Composer/packages/ui-plugins/composer/package.json +++ b/Composer/packages/ui-plugins/composer/package.json @@ -24,7 +24,7 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -36,10 +36,10 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message-cli": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { "lodash": "^4.17.19" } -} +} \ No newline at end of file diff --git a/Composer/packages/ui-plugins/cross-trained/package.json b/Composer/packages/ui-plugins/cross-trained/package.json index aa9d055c37..d60cf9b9b6 100644 --- a/Composer/packages/ui-plugins/cross-trained/package.json +++ b/Composer/packages/ui-plugins/cross-trained/package.json @@ -22,7 +22,7 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -30,7 +30,7 @@ "@bfc/extension-client": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/example/package.json b/Composer/packages/ui-plugins/example/package.json index 717b23aa47..3322fbc7c6 100644 --- a/Composer/packages/ui-plugins/example/package.json +++ b/Composer/packages/ui-plugins/example/package.json @@ -23,7 +23,7 @@ "@uifabric/icons": "^7.3.0", "@uifabric/styling": "^7.7.4", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@emotion/core": "^10.0.27", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/lg/package.json b/Composer/packages/ui-plugins/lg/package.json index 28f861c712..142e105925 100644 --- a/Composer/packages/ui-plugins/lg/package.json +++ b/Composer/packages/ui-plugins/lg/package.json @@ -23,7 +23,7 @@ "peerDependencies": { "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -32,7 +32,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/luis/package.json b/Composer/packages/ui-plugins/luis/package.json index 3d31df3d17..6eeb2588bf 100644 --- a/Composer/packages/ui-plugins/luis/package.json +++ b/Composer/packages/ui-plugins/luis/package.json @@ -24,7 +24,7 @@ "@bfc/adaptive-form": "*", "@bfc/extension-client": "*", "@bfc/shared": "*", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@bfc/test-utils": "*", "@types/react": "16.9.23", "@types/react-dom": "16.9.5", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/prompts/package.json b/Composer/packages/ui-plugins/prompts/package.json index c7c02f27d6..8d4d6e0840 100644 --- a/Composer/packages/ui-plugins/prompts/package.json +++ b/Composer/packages/ui-plugins/prompts/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/schema-editor/package.json b/Composer/packages/ui-plugins/schema-editor/package.json index 2cb8856a81..caee894f71 100644 --- a/Composer/packages/ui-plugins/schema-editor/package.json +++ b/Composer/packages/ui-plugins/schema-editor/package.json @@ -24,7 +24,7 @@ "@bfc/adaptive-form": "*", "@bfc/code-editor": "*", "format-message": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -34,7 +34,7 @@ "@types/react": "16.9.23", "@types/react-dom": "16.9.5", "format-message": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/select-dialog/package.json b/Composer/packages/ui-plugins/select-dialog/package.json index 87ba3cccde..b507dac159 100644 --- a/Composer/packages/ui-plugins/select-dialog/package.json +++ b/Composer/packages/ui-plugins/select-dialog/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/package.json b/Composer/packages/ui-plugins/select-skill-dialog/package.json index cbcd570eaf..c05e3d53fe 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/package.json +++ b/Composer/packages/ui-plugins/select-skill-dialog/package.json @@ -28,7 +28,7 @@ "@uifabric/styling": "^7.7.4", "format-message": "^6.2.3", "office-ui-fabric-react": "^7.71.0", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "@types/react-dom": "16.9.5", "format-message": "^6.2.3", "format-message-cli": "^6.2.3", - "react": "16.13.0", + "react": "16.13.1", "react-dom": "16.13.1" }, "dependencies": { diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 6bd0f20f06..bee4dd509f 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -16018,10 +16018,10 @@ react-timeago@^4.4.0: resolved "https://botbuilder.myget.org/F/botbuilder-tools-daily/npm/react-timeago/-/react-timeago-4.4.0.tgz#4520dd9ba63551afc4d709819f52b14b9343ba2b" integrity sha1-RSDdm6Y1Ua/E1wmBn1KxS5NDuis= -react@16.13.0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7" - integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ== +react@16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" From 6d766576c66d691535d95ef13c5ebcc036802cc1 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 2 Oct 2020 13:32:57 -0700 Subject: [PATCH 016/100] Rename api handlers Signed-off-by: Srinaath Ravichandran --- Composer/package.json | 2 +- .../__tests__/components/skill.test.tsx | 4 +-- .../src/components/CreateSkillModal.tsx | 2 +- .../src/recoilModel/dispatchers/setting.ts | 2 +- .../recoilModel/dispatchers/utils/project.ts | 4 +-- .../server/src/controllers/project.ts | 27 +++++++++++++++++-- Composer/packages/server/src/router/api.ts | 5 ++-- 7 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Composer/package.json b/Composer/package.json index 4f5d112940..f73d7a06f0 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "build:plugins:runtimes": "cd plugins/runtimes && yarn install && yarn build", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", "startall": "yarn start", - "start:dev": "concurrently \"npm:start:client\" \"npm:start:server\"", + "start:dev": "concurrently \"npm:start:client\" \"npm:start:server:dev\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", "start:server:dev": "yarn workspace @bfc/server start:dev", diff --git a/Composer/packages/client/__tests__/components/skill.test.tsx b/Composer/packages/client/__tests__/components/skill.test.tsx index 67b0bdf4b1..f25044478a 100644 --- a/Composer/packages/client/__tests__/components/skill.test.tsx +++ b/Composer/packages/client/__tests__/components/skill.test.tsx @@ -223,7 +223,7 @@ describe('', () => { manifestUrl: 'Validating', }) ); - expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieveSkillManifest`, { params: { url: formData.manifestUrl, }, @@ -261,7 +261,7 @@ describe('', () => { manifestUrl: 'Validating', }) ); - expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + expect(httpClient.get).toBeCalledWith(`/projects/${projectId}/skill/retrieveSkillManifest`, { params: { url: formData.manifestUrl, }, diff --git a/Composer/packages/client/src/components/CreateSkillModal.tsx b/Composer/packages/client/src/components/CreateSkillModal.tsx index f42a21b491..0a900a564e 100644 --- a/Composer/packages/client/src/components/CreateSkillModal.tsx +++ b/Composer/packages/client/src/components/CreateSkillModal.tsx @@ -83,7 +83,7 @@ export const validateManifestUrl = async ({ } else { try { setValidationState({ ...validationState, manifestUrl: ValidationState.Validating }); - const { data } = await httpClient.get(`/projects/${projectId}/skill/retrieve-skill-manifest`, { + const { data } = await httpClient.get(`/projects/${projectId}/skill/retrieveSkillManifest`, { params: { url: manifestUrl, }, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts index d13761e46b..df4b388635 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts @@ -30,7 +30,7 @@ export const setSettingState = async ( 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`, { + const { data: content } = await httpClient.get(`/projects/${projectId}/skill/retrieveSkillManifest`, { params: { url: settings?.skill?.[id]?.manifestUrl, }, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index e77c29f195..b6cb742f59 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -328,12 +328,12 @@ export const openRemoteSkill = async ( ) => { const { set } = callbackHelpers; - const response = await httpClient.get(`/projects/generate-projectId`); + const response = await httpClient.get(`/projects/generateProjectId`); const projectId = response.data; const stringified = stringify({ url: manifestUrl, }); - const manifestResponse = await httpClient.get(`/projects/${projectId}/skill/retrieve-skill-manifest?${stringified}`); + const manifestResponse = await httpClient.get(`/projects/${projectId}/skill/retrieveSkillManifest?${stringified}`); set(projectMetaDataState(projectId), { isRootBot: false, isRemote: true, diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 354637c71b..562574c7c3 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -122,6 +122,12 @@ async function removeProject(req: Request, res: Response) { } async function openProject(req: Request, res: Response) { + if (!req.body.storageId || !req.body.path) { + res.status(400).json({ + message: 'parameters not provided, require stoarge id and path', + }); + return; + } const user = await ExtensionContext.getUserFromRequest(req); const location: LocationRef = { @@ -258,8 +264,25 @@ async function removeFile(req: Request, res: Response) { } async function getSkill(req: Request, res: Response) { - const content = await getSkillManifest(req.query.url); - res.status(200).json(content); + const projectId = req.params.projectId; + const user = await ExtensionContext.getUserFromRequest(req); + const ignoreProjectValidation: boolean = req.query.ignoreProjectValidation; + if (!ignoreProjectValidation) { + const currentProject = await BotProjectService.getProjectById(projectId, user); + if (currentProject === undefined) { + res.status(404).json({ + message: 'No such bot project opened', + }); + } + } + try { + const content = await getSkillManifest(req.query.url); + res.status(200).json(content); + } catch (err) { + res.status(404).json({ + message: err.message, + }); + } } async function exportProject(req: Request, res: Response) { diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index b6632b784d..34156e889f 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -20,7 +20,7 @@ const router: Router = express.Router({}); router.post('/projects', ProjectController.createProject); router.get('/projects', ProjectController.getAllProjects); router.get('/projects/recent', ProjectController.getRecentProjects); -router.get('/projects/generate-projectId', ProjectController.generateProjectId); +router.get('/projects/generateProjectId', ProjectController.generateProjectId); router.get('/projects/:projectId', ProjectController.getProjectById); router.put('/projects/open', ProjectController.openProject); @@ -28,7 +28,8 @@ 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.get('/projects/:projectId/skill/retrieve-skill-manifest', ProjectController.getSkill); +router.get('/projects/:projectId/skill/retrieveSkillManifest', ProjectController.getSkill); +router.get('/projects/skill/retrieveSkillManifest', 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); From 86d74435cb51222bab0347fd67f326074c57cc12 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 2 Oct 2020 18:58:24 -0700 Subject: [PATCH 017/100] Add existing skill tests passing Signed-off-by: Srinaath Ravichandran --- .../__tests__/mocks/mockProjectResponse.json | 4 +- .../dispatchers/__tests__/project.test.tsx | 124 ++++++++++++------ .../dispatchers/__tests__/storage.test.tsx | 93 +++++++++++++ .../recoilModel/dispatchers/utils/project.ts | 1 + .../__mocks__/samplebots/bot1/bot1.botproj | 6 + .../a/knowledge-base/en-us/a.en-us.qna | 0 .../b/knowledge-base/en-us/b.en-us.qna | 0 .../root/knowledge-base/en-us/root.en-us.qna | 0 .../bot1/knowledge-base/en-us/bot1.en-us.qna | 0 .../__mocks__/samplebots/copy/bot1.botproj | 6 + .../src/__mocks__/samplebots/copy/bot1.dialog | 40 ++++++ .../samplebots/copy/dialogs/a/a.dialog | 1 + .../a/knowledge-base/en-us/a.en-us.qna | 0 .../a/language-generation/en-us/a.en-us.lg | 0 .../a/language-understanding/en-us/a.en-us.lu | 0 .../samplebots/copy/dialogs/b/b.dialog | 3 + .../b/knowledge-base/en-us/b.en-us.qna | 0 .../b/language-generation/en-us/b.en-us.lg | 0 .../b/language-understanding/en-us/b.en-us.lu | 0 .../root/knowledge-base/en-us/root.en-us.qna | 0 .../copy/knowledge-base/en-us/bot1.en-us.qna | 0 .../language-generation/en-us/bot1.en-us.lg | 0 .../language-generation/en-us/common.en-us.lg | 14 ++ .../en-us/bot1.en-us.lu | 2 + .../samplebots/copy/settings/appsettings.json | 28 ++++ .../packages/server/src/services/project.ts | 3 - 26 files changed, 280 insertions(+), 45 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/__tests__/storage.test.tsx create mode 100644 Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj create mode 100644 Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/knowledge-base/en-us/a.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/knowledge-base/en-us/b.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/root/knowledge-base/en-us/root.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/bot1/knowledge-base/en-us/bot1.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu create mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json index 0cdf902bb6..6441576ad1 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json @@ -6,8 +6,8 @@ { "name": "emptybot-1.dialog", "content": "{\n \"$kind\": \"Microsoft.AdaptiveDialog\",\n \"$designer\": {\n \"name\": \"AddItem\",\n \"id\": \"225905\"\n },\n \"autoEndDialog\": true,\n \"defaultResultProperty\": \"dialog.result\",\n \"triggers\": [\n {\n \"$kind\": \"Microsoft.OnBeginDialog\",\n \"$designer\": {\n \"name\": \"BeginDialog\",\n \"id\": \"479346\"\n },\n \"actions\": [\n {\n \"$kind\": \"Microsoft.SetProperties\",\n \"$designer\": {\n \"id\": \"811190\",\n \"name\": \"Set properties\"\n },\n \"assignments\": [\n {\n \"property\": \"dialog.itemTitle\",\n \"value\": \"=coalesce(@itemTitle, $itemTitle)\"\n },\n {\n \"property\": \"dialog.listType\",\n \"value\": \"=coalesce(@listType, $listType)\"\n }\n ]\n },\n {\n \"$kind\": \"Microsoft.TextInput\",\n \"$designer\": {\n \"id\": \"282825\",\n \"name\": \"AskForTitle\"\n },\n \"prompt\": \"${TextInput_Prompt_282825()}\",\n \"maxTurnCount\": \"3\",\n \"property\": \"dialog.itemTitle\",\n \"value\": \"=coalesce(@itemTitle, $itemTitle)\",\n \"allowInterruptions\": \"!@itemTitle && #_Interruption.Score >= 0.9\"\n },\n {\n \"$kind\": \"Microsoft.ChoiceInput\",\n \"$designer\": {\n \"id\": \"878594\",\n \"name\": \"AskForListType\"\n },\n \"prompt\": \"${TextInput_Prompt_878594()}\",\n \"maxTurnCount\": \"3\",\n \"property\": \"dialog.listType\",\n \"value\": \"=@listType\",\n \"allowInterruptions\": \"!@listType\",\n \"outputFormat\": \"value\",\n \"choices\": [\n {\n \"value\": \"todo\",\n \"synonyms\": [\n \"to do\"\n ]\n },\n {\n \"value\": \"grocery\",\n \"synonyms\": [\n \"groceries\"\n ]\n },\n {\n \"value\": \"shopping\",\n \"synonyms\": [\n \"shoppers\"\n ]\n }\n ],\n \"appendChoices\": \"true\",\n \"defaultLocale\": \"en-us\",\n \"style\": \"Auto\",\n \"choiceOptions\": {\n \"inlineSeparator\": \", \",\n \"inlineOr\": \" or \",\n \"inlineOrMore\": \", or \",\n \"includeNumbers\": true\n },\n \"recognizerOptions\": {\n \"noValue\": false\n }\n },\n {\n \"$kind\": \"Microsoft.EditArray\",\n \"$designer\": {\n \"id\": \"733511\",\n \"name\": \"Edit an Array property\"\n },\n \"changeType\": \"push\",\n \"itemsProperty\": \"user.lists[dialog.listType]\",\n \"value\": \"=$itemTitle\"\n },\n {\n \"$kind\": \"Microsoft.SendActivity\",\n \"$designer\": {\n \"id\": \"139532\",\n \"name\": \"Send a response\"\n },\n \"activity\": \"${SendActivity_139532()}\"\n }\n ]\n }\n ],\n \"generator\": \"additem.lg\",\n \"recognizer\": \"additem.lu\"\n}\n", - "path": "/Users/tester/Desktop/EmptyBot-1/dialogs/additem/additem.dialog", - "relativePath": "dialogs/additem/additem.dialog", + "path": "/Users/tester/Desktop/EmptyBot-1/additem.dialog", + "relativePath": "", "lastModified": "Thu Jul 09 2020 10:19:09 GMT-0700 (Pacific Daylight Time)" }, { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index ded50ddb3c..f7e8cee048 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useRecoilValue } from 'recoil'; +import { selector, useRecoilValue } from 'recoil'; import { act, RenderHookResult, HookResult } from '@bfc/test-utils/lib/hooks'; import { useRecoilState } from 'recoil'; +import cloneDeep from 'lodash/cloneDeep'; +import endsWith from 'lodash/endsWith'; import httpClient from '../../../utils/httpUtil'; import { projectDispatcher } from '../project'; @@ -33,12 +35,15 @@ import { botDisplayNameState, botOpeningState, botProjectFileState, + botProjectIdsState, + botNameIdentifierState, + botErrorState, } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '../../dispatchers'; import { BotStatus } from '../../../constants'; -import mockProjectResponse from './mocks/mockProjectResponse.json'; +import mockResponse from './mocks/mockProjectResponse.json'; // let httpMocks; let navigateTo; @@ -75,6 +80,25 @@ jest.mock('../../persistence/FilePersistence', () => { }); describe('Project dispatcher', () => { + let mockProjectResponse; + const botStatesSelector = selector({ + key: 'botStatesSelector', + get: ({ get }) => { + const botProjectIds = get(botProjectIdsState); + const botProjectData: { [projectName: string]: { botDisplayName: string; botError: any } } = {}; + botProjectIds.map((projectId) => { + const botDisplayName = get(botDisplayNameState(projectId)); + const botNameIdentifier = get(botNameIdentifierState(projectId)); + const botError = get(botErrorState(projectId)); + + botProjectData[botNameIdentifier] = { + botDisplayName, + botError, + }; + }); + return botProjectData; + }, + }); const useRecoilTestHook = () => { const schemas = useRecoilValue(schemasState(projectId)); const location = useRecoilValue(locationState(projectId)); @@ -89,6 +113,7 @@ describe('Project dispatcher', () => { const diagnostics = useRecoilValue(botDiagnosticsState(projectId)); const locale = useRecoilValue(localeState(projectId)); const botStatus = useRecoilValue(botStatusState(projectId)); + const botStates = useRecoilValue(botStatesSelector); const currentDispatcher = useRecoilValue(dispatcherState); const [recentProjects, setRecentProjects] = useRecoilState(recentProjectsState); @@ -128,13 +153,15 @@ describe('Project dispatcher', () => { botProjectFile, setBotProjectFile, setRecentProjects, + botStates, }; }; let renderedComponent: HookResult>, dispatcher: Dispatcher; - beforeEach(() => { + beforeEach(async () => { navigateTo.mockReset(); + mockProjectResponse = cloneDeep(mockResponse); const rendered: RenderHookResult> = renderRecoilHook( useRecoilTestHook, { @@ -153,17 +180,19 @@ describe('Project dispatcher', () => { }); it('should throw an error if no bot project file is present in the bot', async () => { + const cloned = cloneDeep(mockProjectResponse); + const filtered = cloned.files.filter((file) => !endsWith(file.name, '.botproj')); + cloned.files = filtered; (httpClient.put as jest.Mock).mockResolvedValueOnce({ - data: mockProjectResponse, + data: cloned, }); await act(async () => { await dispatcher.openProject('../test/empty-bot', 'default'); }); expect(navigateTo).toHaveBeenLastCalledWith(`/home`); - expect(renderedComponent.current.appError).toBeUndefined(); }); - fit('should open bot project', async () => { + it('should open bot project', async () => { (httpClient.put as jest.Mock).mockResolvedValueOnce({ data: mockProjectResponse, }); @@ -182,7 +211,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.schemas.sdk).toBeDefined(); expect(renderedComponent.current.schemas.default).toBeDefined(); expect(renderedComponent.current.schemas.diagnostics?.length).toBe(0); - expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/`); + expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); }); it('should handle project failure if project does not exist', async () => { @@ -199,10 +228,10 @@ describe('Project dispatcher', () => { ]); await dispatcher.openProject('../test/empty-bot', 'default'); }); - // expect(renderedComponent.current.botOpening).toBeFalsy(); + expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.appError).toEqual(errorObj); expect(renderedComponent.current.recentProjects.length).toBe(0); - expect(navigateTo).not.toHaveBeenCalled(); + expect(navigateTo).toHaveBeenLastCalledWith(`/home`); }); it('should fetch recent projects', async () => { @@ -215,36 +244,6 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.recentProjects).toEqual(recentProjects); }); - it('should get runtime templates', async () => { - const templates = [ - { id: 'EchoBot', index: 1, name: 'Echo Bot' }, - { id: 'EmptyBot', index: 2, name: 'Empty Bot' }, - ]; - (httpClient.get as jest.Mock).mockResolvedValue({ - data: templates, - }); - await act(async () => { - await dispatcher.fetchRuntimeTemplates(); - }); - - expect(renderedComponent.current.runtimeTemplates).toEqual(templates); - }); - - it('should get templates', async () => { - const templates = [ - { id: 'EchoBot', index: 1, name: 'Echo Bot' }, - { id: 'EmptyBot', index: 2, name: 'Empty Bot' }, - ]; - (httpClient.get as jest.Mock).mockResolvedValue({ - data: templates, - }); - await act(async () => { - await dispatcher.fetchTemplates(); - }); - - expect(renderedComponent.current.templates).toEqual(templates); - }); - it('should delete a project', async () => { (httpClient.delete as jest.Mock).mockResolvedValue({ data: {} }); (httpClient.put as jest.Mock).mockResolvedValueOnce({ @@ -292,7 +291,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.announcement).toEqual('Scripts successfully updated.'); }); - it('should get bolierplate version', async () => { + it('should get boilerplate version', async () => { const version = { updateRequired: true, latestVersion: '3', currentVersion: '2' }; (httpClient.get as jest.Mock).mockResolvedValue({ data: version, @@ -303,4 +302,49 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.boilerplateVersion).toEqual(version); }); + + it('should be able to add an existing skill to Botproject and a remote skill', async () => { + const skills = [ + { botName: 'Echo-Skill-1', id: '40876.502871204648', location: '/Users/tester/Desktop/Echo-Skill-1' }, + { botName: 'Echo-Skill-2', id: '50876.502871204648', location: '/Users/tester/Desktop/Echo-Skill-2' }, + ]; + const mappedSkills = skills.map(({ botName, id, location }) => { + const cloned = cloneDeep(mockProjectResponse); + return { + ...cloned, + botName, + id, + location, + }; + }); + + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mockProjectResponse, + }); + + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mappedSkills[0], + }); + await dispatcher.addExistingSkillToBotProject(mappedSkills[0].location, 'default'); + }); + + expect(renderedComponent.current.botStates.echoSkill1).toBeDefined(); + expect(renderedComponent.current.botStates.echoSkill1.botDisplayName).toBe('Echo-Skill-1'); + + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mappedSkills[1], + }); + await dispatcher.addExistingSkillToBotProject(mappedSkills[1].location, 'default'); + }); + + expect(renderedComponent.current.botStates.echoSkill2).toBeDefined(); + expect(renderedComponent.current.botStates.echoSkill2.botDisplayName).toBe('Echo-Skill-2'); + expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); + }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/storage.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/storage.test.tsx new file mode 100644 index 0000000000..696a1b53ab --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/storage.test.tsx @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useRecoilValue } from 'recoil'; +import { act, RenderHookResult, HookResult } from '@bfc/test-utils/lib/hooks'; + +import httpClient from '../../../utils/httpUtil'; +import { storageDispatcher } from '../storage'; +import { renderRecoilHook } from '../../../../__tests__/testUtils'; +import { runtimeTemplatesState, currentProjectIdState } from '../../atoms'; +import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; +import { Dispatcher } from '../../dispatchers'; + +// let httpMocks; +let navigateTo; + +const projectId = '30876.502871204648'; + +jest.mock('../../../utils/navigation', () => { + const navigateMock = jest.fn(); + navigateTo = navigateMock; + return { + navigateTo: navigateMock, + }; +}); + +jest.mock('../../../utils/httpUtil'); + +jest.mock('../../parsers/lgWorker', () => { + return { + flush: () => new Promise((resolve) => resolve()), + addProject: () => new Promise((resolve) => resolve()), + }; +}); + +jest.mock('../../parsers/luWorker', () => { + return { + flush: () => new Promise((resolve) => resolve()), + }; +}); + +jest.mock('../../persistence/FilePersistence', () => { + return jest.fn().mockImplementation(() => { + return { flush: () => new Promise((resolve) => resolve()) }; + }); +}); + +describe('Storage dispatcher', () => { + const useRecoilTestHook = () => { + const runtimeTemplates = useRecoilValue(runtimeTemplatesState); + const currentDispatcher = useRecoilValue(dispatcherState); + + return { + runtimeTemplates, + currentDispatcher, + }; + }; + + let renderedComponent: HookResult>, dispatcher: Dispatcher; + + beforeEach(() => { + navigateTo.mockReset(); + const rendered: RenderHookResult> = renderRecoilHook( + useRecoilTestHook, + { + states: [{ recoilState: currentProjectIdState, initialValue: projectId }], + dispatcher: { + recoilState: dispatcherState, + initialValue: { + storageDispatcher, + }, + }, + } + ); + renderedComponent = rendered.result; + dispatcher = renderedComponent.current.currentDispatcher; + }); + + it('should get runtime templates', async () => { + const templates = [ + { id: 'EchoBot', index: 1, name: 'Echo Bot' }, + { id: 'EmptyBot', index: 2, name: 'Empty Bot' }, + ]; + (httpClient.get as jest.Mock).mockResolvedValue({ + data: templates, + }); + await act(async () => { + await dispatcher.fetchRuntimeTemplates(); + }); + + expect(renderedComponent.current.runtimeTemplates).toEqual(templates); + }); +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index b6cb742f59..81046ae22a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -267,6 +267,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any try { schemas.sdk.content = processSchema(projectId, schemas.sdk.content); } catch (err) { + console.log('Error', err); const diagnostics = schemas.diagnostics ?? []; diagnostics.push(err.message); schemas.diagnostics = diagnostics; diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj b/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj new file mode 100644 index 0000000000..5aeb3fb004 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", + "name": "bot1", + "workspace": "/Users/srravich/Projects/BotFramework-Composer/Composer/packages/server/src/__mocks__/samplebots/bot1", + "skills": {} +} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/knowledge-base/en-us/a.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/knowledge-base/en-us/a.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/knowledge-base/en-us/b.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/knowledge-base/en-us/b.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/root/knowledge-base/en-us/root.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/root/knowledge-base/en-us/root.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/knowledge-base/en-us/bot1.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/bot1/knowledge-base/en-us/bot1.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj new file mode 100644 index 0000000000..5aeb3fb004 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj @@ -0,0 +1,6 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", + "name": "bot1", + "workspace": "/Users/srravich/Projects/BotFramework-Composer/Composer/packages/server/src/__mocks__/samplebots/bot1", + "skills": {} +} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog new file mode 100644 index 0000000000..1c7084afda --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog @@ -0,0 +1,40 @@ +{ + "old": "value", + "steps": [ + { + "$kind": "Microsoft.TextInput", + "prompt": "${hello()} I'm a ToDo bot" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "${bye()} See you again" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "${bye()} bye bye again" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "${ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)}" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "You entered: ${user.date[0].value}" + }, + { + "$kind": "Microsoft.TextInput", + "activity": "${bye3()} See you again" + }, + { + "$kind": "Microsoft.OnIntent", + "intent": "ShowIntent", + "steps": [ + { + "$kind": "Microsoft.BeginDialog", + "dialog": "a" + } + ] + } + ], + "generator": "bot1.lg" +} diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog new file mode 100644 index 0000000000..04ef3aeed1 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog @@ -0,0 +1 @@ +{"old":"value"} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog new file mode 100644 index 0000000000..e6defc9b28 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog @@ -0,0 +1,3 @@ +{ + "generator": "b.lg" +} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg new file mode 100644 index 0000000000..08f7572efa --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg @@ -0,0 +1,14 @@ +# greet + - Hello! + +# hello + - Hello Welcome! + +# bye + - Good bye! + +# ShowImage(contentUrl, contentType) +- ``` +You sent: +[attachment=@{contentUrl} @{contentType}] +``` \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu new file mode 100644 index 0000000000..df74df7d1e --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu @@ -0,0 +1,2 @@ +# Greeting +-hi \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json b/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json new file mode 100644 index 0000000000..e468dd1746 --- /dev/null +++ b/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json @@ -0,0 +1,28 @@ +{ + "MicrosoftAppId": "", + "luis": { + "name": "", + "authoringRegion": "westus", + "defaultLanguage": "en-us", + "environment": "composer", + "endpoint": "", + "authoringEndpoint": "", + "authoringKey": "", + "endpointKey": "" + }, + "qna": { + "knowledgebaseid": "", + "hostname": "", + "endpointKey": "", + "subscriptionKey": "" + }, + "downsampling": { + "maxImbalanceRatio": 10, + "maxUtteranceAllowed": 15000 + }, + "defaultLanguage": "en-us", + "languages": [ + "en-us" + ], + "MicrosoftAppPassword": "" +} \ No newline at end of file diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts index 9aebf5571e..31d0eaac8e 100644 --- a/Composer/packages/server/src/services/project.ts +++ b/Composer/packages/server/src/services/project.ts @@ -239,9 +239,6 @@ export class BotProjectService { }; private static addRecentProject = (path: string): void => { - // if (!BotProjectService.currentBotProject) { - // return; - // } const currDir = Path.resolve(path); const idx = BotProjectService.recentBotProjects.findIndex((ref) => currDir === Path.resolve(ref.path)); if (idx > -1) { From 308e3920fa89666f982a6f957de9135d2d805db1 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sun, 4 Oct 2020 14:17:54 -0700 Subject: [PATCH 018/100] Add 1.0 botproject schema Signed-off-by: Srinaath Ravichandran --- Composer/.gitignore | 1 + .../packages/server/schemas/botproject.schema | 74 ++++++++++++ Schemas/SampleOfficeVA.botProject.json | 24 ---- Schemas/botproject-schema.json | 71 ----------- Schemas/provisioningProfile-schema.json | 112 ------------------ Schemas/sampleProvisionProfile.json | 28 ----- 6 files changed, 75 insertions(+), 235 deletions(-) create mode 100644 Composer/packages/server/schemas/botproject.schema delete mode 100644 Schemas/SampleOfficeVA.botProject.json delete mode 100644 Schemas/botproject-schema.json delete mode 100644 Schemas/provisioningProfile-schema.json delete mode 100644 Schemas/sampleProvisionProfile.json diff --git a/Composer/.gitignore b/Composer/.gitignore index 4a121af957..dd7f3398f4 100644 --- a/Composer/.gitignore +++ b/Composer/.gitignore @@ -13,6 +13,7 @@ packages/server/schemas/*.schema packages/server/schemas/*.uischema !packages/server/schemas/sdk.schema !packages/server/schemas/sdk.uischema +!packages/server/schemas/botproject.schema # remote extensions .composer diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema new file mode 100644 index 0000000000..1ff9e75958 --- /dev/null +++ b/Composer/packages/server/schemas/botproject.schema @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/microsoft/BotFramework-Composer/blob/main/Composer/packages/server/schemas/botproject.schema", + "$version": "1.0.0", + "title": "Root Bot and Skills workspace schema for a environemnt. Each publishing environment has a Bot Project file associated with it.", + "description": "Schema that captures the relationship between the Root Bot and remote/local skills that the Root Bot consumes.", + "type": "object", + "required": ["$schema", "name", "workspace", "skills"], + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "description": "The schema to verify this workspace file." + }, + "name": { + "type": "string", + "description": "Name of the Root Bot." + }, + "workspace": { + "type": "string", + "description": "Path to the Root Bot. If a workspace is local, we use the file protocol as opposed to http/https for remote workspaces." + }, + "skills": { + "description": "List of skills (remote or local) skills that the Root Bot consumes.", + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/skill" + } + }, + "additionalProperties": false + } + }, + "definitions": { + "skill": { + "type": "object", + "description": "Properties of a skill.", + "additionalProperties": false, + "required": ["remote"], + "oneOf": [ + { + "anyOf": [ + { + "required": ["workspace"] + }, + { + "required": ["manifest"] + } + ] + } + ], + "properties": { + "manifest": { + "type": "string", + "description": "URL that points to the manifest exposed by the skill." + }, + "workspace": { + "type": "string", + "description": "Path to a skill workspace. If a workspace is local, we use the file protocol as opposed to http/https protocols for remote workspaces." + }, + "remote": { + "description": "Indication if the skill is remote or local skill.", + "type": "boolean", + "title": "isRemote", + "default": false + }, + "endpointName": { + "type": "string", + "description": "The name of the endpoint inside the above chosen manifest that the Root Bot can use to consume the skill capabilities." + } + } + } + } +} diff --git a/Schemas/SampleOfficeVA.botProject.json b/Schemas/SampleOfficeVA.botProject.json deleted file mode 100644 index 938622e215..0000000000 --- a/Schemas/SampleOfficeVA.botProject.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", - "name": "OfficeVA", - "workspace": "file://../Projects/OfficeVA", - "provisioningProfile": "file://../Projects/DeploymentProfiles/officeVA.json", - "skills": [ - { - "workspace": "file://../Projects/RoomSchedulerSkill", - "manifest": "room-scheduler-skill-manifest-v2.1.0", - "provisioningProfile": "file://../Projects/DeploymentProfiles/roomSchedulerProfile.json", - "remote": false, - "endpointName": "local" - }, - { - "manifest": "https://calendar-skill/calendar-skill-manifest-v2.1.0.json", - "remote": true - }, - { - "workspace": "file://../Projects/LunchOrderingSkill", - "provisioningProfile": "file://../Projects/DeploymentProfiles/lunchOrderingSkillProfile.json", - "remote": false - } - ] -} diff --git a/Schemas/botproject-schema.json b/Schemas/botproject-schema.json deleted file mode 100644 index 2d4356adb2..0000000000 --- a/Schemas/botproject-schema.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$id": "https://schemas.botframework.com/schemas/botprojects/v0.1/worskspace.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "$version": "0.1", - "title": "Virtual Assistant and Skills workspace schema", - "description": "This is a schema that captures the relationship between the virtual assistant and its skills.", - "type": "object", - "required": ["$schema", "name", "workspace", "skills"], - "properties": { - "$schema": { - "type": "string", - "format": "uri", - "description": "The schema to verify this workspace file against" - }, - "name": { - "type": "string", - "description": "Name of the Virtual Assistant or Root Bot." - }, - "workspace": { - "type": "string", - "description": "Path to the virtual assistant or Root Bot. Relative to the workspace file or an absolute path." - }, - "provisioningProfile": { - "type": "string", - "description": "Path to provisioning profile." - }, - "skills": { - "type": "array", - "minItems": 0, - "uniqueItems": true, - "description": "List of skills callable.", - "items": { - "$ref": "#/definitions/skill" - } - } - }, - "definitions": { - "skill": { - "type": "object", - "description": "Properties of a skill.", - "additionalProperties": false, - "required": ["manifest", "remote"], - "properties": { - "manifest": { - "type": "string", - "description": "Path to the manifest file on local file system or a URL to a remote skill." - }, - "workspace": { - "type": "string", - "description": "Path to the local skill project. If the workspace is local we use the file protocol as opposed to http/https for remote workspaces." - }, - "remote": { - "type": "string", - "description": "Indication if the skill is remote or local skill.", - "$ref": "#/definitions/booleanExpression", - "title": "isRemote", - "default": false - }, - "endpointName": { - "type": "string", - "description": "Indicates the name of the skill endpoint to use for writing the skill URL. If skill is remote no need for this property.", - "default": "default" - }, - "provisioningProfile": { - "type": "string", - "description": "Path to provisioning profile." - } - } - } - } -} diff --git a/Schemas/provisioningProfile-schema.json b/Schemas/provisioningProfile-schema.json deleted file mode 100644 index a0e15ef140..0000000000 --- a/Schemas/provisioningProfile-schema.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "$id": "https://schemas.botframework.com/schemas/botprojects/v0.1/provisioning.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "$version": "0.1", - "title": "Provisioning profile schema for Bot Projects", - "description": "This is a schema that captures the resource provisioning needs for a bot which maybe a Virtual Assistant or a skill", - "type": "object", - "required": [ - "$schema", - "MicrosoftAppId", - "MicrosoftAppPassword", - "accessToken" - ], - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "format": "uri", - "description": "The schema to verify this provisioning profile file against" - }, - "MicrosoftAppId": { - "type": "string", - "description": "Micorosft App ID of the resource" - }, - "MicrosoftAppPassword": { - "type": "string", - "description": "Micorosft App Password of the resource" - }, - "name": { - "type": "string", - "description": "Optional name of the publishing target" - }, - "accessToken": { - "type": "string", - "description": "Access token for azure account" - }, - "applicationInsights": { - "type": "object", - "description": "Application Insights provisioning", - "properties": { - "endpointKey": { - "type": "string", - "description": "Instrumentation key for application insights" - } - } - }, - "blobStorage": { - "type": "object", - "description": "Configuring blob storage on azure", - "properties": { - "connectionString": { - "type": "string", - "description": "Connection string for blob storage" - } - } - }, - "luis": { - "type": "object", - "description": "LUIS provisioning for a bot", - "properties": { - "endpointKey": { - "type": "string", - "description": "Endpoint key for LUIS provisioning" - }, - "authoringKey": { - "type": "string", - "description": "Authoring key for LUIS provisioning" - }, - "region": { - "type": "string", - "description": "Region for LUIS provisioning" - }, - "endpoint": { - "type": "string", - "description": "LUIS endpoint configuration" - }, - "authoringEndpoint": { - "type": "string", - "description": "Authoring endpoint for LUIS" - } - } - }, - "qna": { - "type": "object", - "description": "Qna maker provisioning for a bot", - "properties": { - "knowledgebaseid": { - "type": "string", - "description": "Qna maker knowledge base id" - }, - "endpoint": { - "type": "string", - "description": "Hostname for QNA maker resource" - } - } - }, - "cosmosDb": { - "type": "object", - "description": "Cosmos db provisioning for a bot", - "properties": { - "endpoint": { - "type": "string", - "description": "Endpoint configuration for cosmos db" - }, - "authKey": { - "type": "string", - "description": "Authorization key for cosmos db" - } - } - } - } -} diff --git a/Schemas/sampleProvisionProfile.json b/Schemas/sampleProvisionProfile.json deleted file mode 100644 index 9fdfd44688..0000000000 --- a/Schemas/sampleProvisionProfile.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/provisioningProfile-schema.json", - "accessToken": "SDFSDFSDFSDBNASKDJASDJJLKJSADJLKMXRNZW", - "environment": "dev", - "MicrosoftAppId": "fd38ebff-02e2-427c-234234-234234", - "MicrosoftAppPassword": "XYZSASJDLKASjksdhfks!sfs", - "blobStorage": { - "connectionString": "DefaultEndpointsProtocol=https;AccountName=SampleSkill" - }, - "cosmosDb": { - "endpoint": "https://todoluissample2-dev.documents.azure.com:443/", - "authKey": "XSFDDSFOHOI" - }, - "qna": { - "knowledgebaseId": "6ea9080e0dsfsdfsdfb4c44", - "endpoint": "https://composerdemovenky.cognitiveservices.azure.com/qnamaker/v4.0/knowledgebases/89366431-a501-324324-asdhas" - }, - "luis": { - "endpointKey": "234kj23h4asdjajdkjashdkjAAsad", - "authoringKey": "234kjh890aasdhakjs", - "region": "westus", - "endpoint": "https://westus.api.cognitive.microsoft.com/", - "authoringEndpoint": "https://westus.api.cognitive.microsoft.com/" - }, - "applicationInsights": { - "endpointKey": "Asdfsdfa23432adsfsdf!sdsad" - } -} From b98e1faa4c5c5d5d72955fe59cd155f5ae8e30c4 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sun, 4 Oct 2020 14:22:46 -0700 Subject: [PATCH 019/100] Fix typo Signed-off-by: Srinaath Ravichandran --- Composer/packages/server/schemas/botproject.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema index 1ff9e75958..745c54f133 100644 --- a/Composer/packages/server/schemas/botproject.schema +++ b/Composer/packages/server/schemas/botproject.schema @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://github.com/microsoft/BotFramework-Composer/blob/main/Composer/packages/server/schemas/botproject.schema", "$version": "1.0.0", - "title": "Root Bot and Skills workspace schema for a environemnt. Each publishing environment has a Bot Project file associated with it.", + "title": "Root Bot and Skills workspace schema for a environment. Each publishing environment has a Bot Project file associated with it.", "description": "Schema that captures the relationship between the Root Bot and remote/local skills that the Root Bot consumes.", "type": "object", "required": ["$schema", "name", "workspace", "skills"], From 26581e246d69886743e2d0bbe918a5926710e4c5 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sun, 4 Oct 2020 02:19:54 -0700 Subject: [PATCH 020/100] Manifest tests updated Signed-off-by: Srinaath Ravichandran More updates Signed-off-by: Srinaath Ravichandran Revert publish console logs Signed-off-by: Srinaath Ravichandran All unit tests added Signed-off-by: Srinaath Ravichandran --- .../components/CreationFlow/index.test.tsx | 20 +- .../__tests__/botProjectFile.test.tsx | 191 +++++++++++++++++ .../__tests__/mocks/mockBotProjectFile.json | 17 ++ .../__tests__/mocks/mockManifest.json | 25 +++ .../__tests__/mocks/mockProjectResponse.json | 4 +- .../dispatchers/__tests__/project.test.tsx | 196 +++++++++++++++++- .../recoilModel/dispatchers/botProjectFile.ts | 14 -- .../src/recoilModel/dispatchers/publisher.ts | 17 +- .../src/recoilModel/dispatchers/storage.ts | 17 ++ .../recoilModel/dispatchers/utils/project.ts | 29 ++- .../packages/lib/indexers/src/skillIndexer.ts | 1 + .../__mocks__/samplebots/copy/bot1.botproj | 6 - .../src/__mocks__/samplebots/copy/bot1.dialog | 40 ---- .../samplebots/copy/dialogs/a/a.dialog | 1 - .../a/knowledge-base/en-us/a.en-us.qna | 0 .../a/language-generation/en-us/a.en-us.lg | 0 .../a/language-understanding/en-us/a.en-us.lu | 0 .../samplebots/copy/dialogs/b/b.dialog | 3 - .../b/knowledge-base/en-us/b.en-us.qna | 0 .../b/language-generation/en-us/b.en-us.lg | 0 .../b/language-understanding/en-us/b.en-us.lu | 0 .../root/knowledge-base/en-us/root.en-us.qna | 0 .../copy/knowledge-base/en-us/bot1.en-us.qna | 0 .../language-generation/en-us/bot1.en-us.lg | 0 .../language-generation/en-us/common.en-us.lg | 14 -- .../en-us/bot1.en-us.lu | 2 - .../samplebots/copy/settings/appsettings.json | 28 --- .../controllers/__tests__/publisher.test.ts | 3 +- .../models/bot/__tests__/botProject.test.ts | 4 +- .../server/src/models/bot/botStructure.ts | 1 - Composer/packages/server/src/router/api.ts | 1 - 31 files changed, 476 insertions(+), 158 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockBotProjectFile.json create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockManifest.json delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu delete mode 100644 Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json diff --git a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx index 0d4e8641d8..826b96694c 100644 --- a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx @@ -15,7 +15,7 @@ describe('', () => { const createProjectMock = jest.fn(); const initRecoilState = ({ set }) => { set(dispatcherState, { - createProject: createProjectMock, + createNewBot: createProjectMock, fetchStorages: jest.fn(), fetchTemplateProjects: jest.fn(), onboardingAddCoachMarkRef: jest.fn(), @@ -70,14 +70,14 @@ describe('', () => { act(() => { fireEvent.click(node); }); - expect(createProjectMock).toHaveBeenCalledWith( - 'EchoBot', - 'EchoBot-1', - '', - expect.stringMatching(/(\/|\\)test-folder(\/|\\)Desktop/), - '', - 'en-US', - undefined - ); + expect(createProjectMock).toHaveBeenCalledWith({ + appLocale: 'en-US', + description: '', + location: '/test-folder/Desktop', + name: 'EchoBot-1', + qnaKbUrls: undefined, + schemaUrl: '', + templateId: 'EchoBot', + }); }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx new file mode 100644 index 0000000000..411f7effd1 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { selector, useRecoilValue, selectorFamily, useRecoilState } from 'recoil'; +import { act, RenderHookResult, HookResult } from '@bfc/test-utils/lib/hooks'; + +import { botProjectFileDispatcher } from '../botProjectFile'; +import { renderRecoilHook } from '../../../../__tests__/testUtils'; +import { + botDisplayNameState, + botErrorState, + botNameIdentifierState, + botProjectFileState, + botProjectIdsState, + currentProjectIdState, + locationState, + projectMetaDataState, +} from '../../atoms'; +import { dispatcherState } from '../../DispatcherWrapper'; +import { Dispatcher } from '..'; + +jest.mock('../../../utils/httpUtil'); +const rootBotProjectId = '2345.32324'; +const testSkillId = '123.1sd23'; + +describe('Bot Project File dispatcher', () => { + const skillsDataSelector = selectorFamily({ + key: 'skillsDataSelector-botProjectFile', + get: (skillId: string) => ({ get }) => { + return { + name: get(botNameIdentifierState(skillId)), + }; + }, + set: (skillId: string) => ({ set }, stateUpdater: any) => { + const { botNameIdentifier, location } = stateUpdater; + set(botNameIdentifierState(skillId), botNameIdentifier); + set(locationState(skillId), location); + }, + }); + + const botStatesSelector = selector({ + key: 'botStatesSelector', + get: ({ get }) => { + const botProjectIds = get(botProjectIdsState); + const botProjectData: { [projectName: string]: { botDisplayName: string; botError: any; location: string } } = {}; + botProjectIds.map((projectId) => { + const botDisplayName = get(botDisplayNameState(projectId)); + const botNameIdentifier = get(botNameIdentifierState(projectId)); + const botError = get(botErrorState(projectId)); + const location = get(locationState(projectId)); + if (botNameIdentifier) { + botProjectData[botNameIdentifier] = { + botDisplayName, + location, + botError, + }; + } + }); + return botProjectData; + }, + }); + + const useRecoilTestHook = () => { + const botName = useRecoilValue(botDisplayNameState(rootBotProjectId)); + const botProjectFile = useRecoilValue(botProjectFileState(rootBotProjectId)); + const currentDispatcher = useRecoilValue(dispatcherState); + const botStates = useRecoilValue(botStatesSelector); + const [skillsData, setSkillsData] = useRecoilState(skillsDataSelector(testSkillId)); + + return { + botName, + currentDispatcher, + botProjectFile, + botStates, + skillsData, + setSkillsData, + }; + }; + + let renderedComponent: HookResult>, dispatcher: Dispatcher; + beforeEach(() => { + const rendered: RenderHookResult> = renderRecoilHook( + useRecoilTestHook, + { + states: [ + { recoilState: currentProjectIdState, initialValue: rootBotProjectId }, + { + recoilState: botProjectFileState(rootBotProjectId), + initialValue: { + content: { + $schema: '', + name: 'TesterBot', + workspace: 'file:///Users/tester/Desktop/LoadedBotProject/TesterBot', + skills: {}, + }, + }, + }, + { + recoilState: projectMetaDataState(rootBotProjectId), + initialValue: { + isRootBot: true, + }, + }, + { recoilState: botProjectIdsState, initialValue: [rootBotProjectId] }, + ], + dispatcher: { + recoilState: dispatcherState, + initialValue: { + botProjectFileDispatcher, + }, + }, + } + ); + renderedComponent = rendered.result; + dispatcher = renderedComponent.current.currentDispatcher; + }); + + it('should add a local skill to bot project file', async () => { + await act(async () => { + renderedComponent.current.setSkillsData({ + location: 'Users/tester/Desktop/LoadedBotProject/Todo-Skill', + botNameIdentifier: 'todoSkill', + }); + }); + + await act(async () => { + dispatcher.addLocalSkillToBotProjectFile(testSkillId); + }); + + expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toBe( + 'file://Users/tester/Desktop/LoadedBotProject/Todo-Skill' + ); + expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.remote).toBeFalsy(); + }); + + it('should add a remote skill to bot project file', async () => { + const manifestUrl = 'https://test-dev.azurewebsites.net/manifests/test-2-1-preview-1-manifest.json'; + await act(async () => { + renderedComponent.current.setSkillsData({ + location: manifestUrl, + botNameIdentifier: 'oneNoteSkill', + }); + }); + + await act(async () => { + dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); + }); + + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.workspace).toBeUndefined(); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.endpointName).toBe('remote'); + }); + + it('should add a remote skill to bot project file', async () => { + const manifestUrl = 'https://test-dev.azurewebsites.net/manifests/test-2-1-preview-1-manifest.json'; + await act(async () => { + renderedComponent.current.setSkillsData({ + location: manifestUrl, + botNameIdentifier: 'oneNoteSkill', + }); + }); + + await act(async () => { + dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); + }); + + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.workspace).toBeUndefined(); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.endpointName).toBe('remote'); + }); + + it('should remove a skill from the bot project file', async () => { + const manifestUrl = 'https://test-dev.azurewebsites.net/manifests/test-2-1-preview-1-manifest.json'; + await act(async () => { + renderedComponent.current.setSkillsData({ + location: manifestUrl, + botNameIdentifier: 'oneNoteSkill', + }); + }); + + await act(async () => { + dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); + }); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); + + await act(async () => { + dispatcher.removeSkillFromBotProjectFile(testSkillId); + }); + expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill).toBeUndefined(); + }); +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockBotProjectFile.json b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockBotProjectFile.json new file mode 100644 index 0000000000..b1bf6ae908 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockBotProjectFile.json @@ -0,0 +1,17 @@ +{ + "$schema": "", + "name": "TesterBot", + "workspace": "file:///Users/tester/Desktop/LoadedBotProject/TesterBot", + "skills": { + "todoSkill": { + "workspace": "file:///Users/tester/Desktop/LoadedBotProject/Todo-Skill", + "manifest": "Todo-Skill-2-1-preview-1-manifest", + "remote": false, + "endpointName": "default" + }, + "googleKeepSync": { + "workspace": "file:///Users/tester/Desktop/LoadedBotProject/GoogleKeepSync", + "remote": false + } + } +} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockManifest.json b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockManifest.json new file mode 100644 index 0000000000..839e03f889 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockManifest.json @@ -0,0 +1,25 @@ + +{ + "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.1.preview-1.json", + "$id": "OneNoteSync", + "name": "OneNoteSync", + "version": "1.0", + "publisherName": "Microsoft", + "description": "Sync notes to OneNote", + "endpoints": [ + { + "name": "default", + "protocol": "BotFrameworkV3", + "description": "Local endpoint for SkillBot.", + "endpointUrl": "http://localhost:3988/api/messages", + "msAppId": "123-b33a9-4b2bb-9d6d-21" + }, + { + "name": "remote", + "protocol": "BotFrameworkV3", + "description": "Production endpoint for SkillBot.", + "endpointUrl": "https://test.net/api/messages", + "msAppId": "123-8138c-43144-8676-21" + } + ] +} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json index 6441576ad1..e815347024 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json @@ -25,7 +25,7 @@ "lastModified": "Thu Jul 09 2020 10:19:09 GMT-0700 (Pacific Daylight Time)" },{ "name": "EmptyBot-1.botproj", - "content": "{\"$schema\":\"https:\/\/schemas.botframework.com\/schemas\/botprojects\/v0.1\/botproject-schema.json\",\"name\":\"echobot-0\",\"workspace\":\"\/Users\/srravich\/Desktop\/samples\/EchoBot-0\",\"skills\":{}}", + "content": "{\"$schema\":\"https:\/\/schemas.botframework.com\/schemas\/botprojects\/v0.1\/botproject-schema.json\",\"name\":\"echobot-0\",\"workspace\":\"\/Users\/tester\/Desktop\/samples\/EchoBot-0\",\"skills\":{}}", "path": "/Users/tester/Desktop/EmptyBot-1/EmptyBot-1.botproj", "relativePath": "dialogs/additem/language-understanding/en-us/additem.en-us.lu", "lastModified": "Thu Jul 09 2020 10:19:09 GMT-0700 (Pacific Daylight Time)" @@ -8629,7 +8629,7 @@ }, "diagnostics": [] }, - "skills": [], + "skills": {}, "diagnostics": [], "settings": { "feature": { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index f7e8cee048..b6d3570cba 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -2,10 +2,13 @@ // Licensed under the MIT License. import { selector, useRecoilValue } from 'recoil'; +import { v4 as uuid } from 'uuid'; import { act, RenderHookResult, HookResult } from '@bfc/test-utils/lib/hooks'; import { useRecoilState } from 'recoil'; import cloneDeep from 'lodash/cloneDeep'; import endsWith from 'lodash/endsWith'; +import findIndex from 'lodash/findIndex'; +import { SkillSetting } from '@bfc/shared/src/types'; import httpClient from '../../../utils/httpUtil'; import { projectDispatcher } from '../project'; @@ -38,12 +41,15 @@ import { botProjectIdsState, botNameIdentifierState, botErrorState, + botProjectSpaceLoadedState, } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '../../dispatchers'; import { BotStatus } from '../../../constants'; -import mockResponse from './mocks/mockProjectResponse.json'; +import mockProjectData from './mocks/mockProjectResponse.json'; +import mockManifestData from './mocks/mockManifest.json'; +import mockBotProjectFileData from './mocks/mockBotProjectFile.json'; // let httpMocks; let navigateTo; @@ -80,25 +86,30 @@ jest.mock('../../persistence/FilePersistence', () => { }); describe('Project dispatcher', () => { - let mockProjectResponse; + let mockProjectResponse, mockManifestResponse, mockBotProjectResponse; const botStatesSelector = selector({ key: 'botStatesSelector', get: ({ get }) => { const botProjectIds = get(botProjectIdsState); - const botProjectData: { [projectName: string]: { botDisplayName: string; botError: any } } = {}; + const botProjectData: { [projectName: string]: any } = {}; botProjectIds.map((projectId) => { const botDisplayName = get(botDisplayNameState(projectId)); const botNameIdentifier = get(botNameIdentifierState(projectId)); const botError = get(botErrorState(projectId)); - - botProjectData[botNameIdentifier] = { - botDisplayName, - botError, - }; + const location = get(locationState(projectId)); + if (botNameIdentifier) { + botProjectData[botNameIdentifier] = { + botDisplayName, + location, + botError, + projectId, + }; + } }); return botProjectData; }, }); + const useRecoilTestHook = () => { const schemas = useRecoilValue(schemasState(projectId)); const location = useRecoilValue(locationState(projectId)); @@ -114,6 +125,7 @@ describe('Project dispatcher', () => { const locale = useRecoilValue(localeState(projectId)); const botStatus = useRecoilValue(botStatusState(projectId)); const botStates = useRecoilValue(botStatesSelector); + const botProjectSpaceLoaded = useRecoilValue(botProjectSpaceLoadedState); const currentDispatcher = useRecoilValue(dispatcherState); const [recentProjects, setRecentProjects] = useRecoilState(recentProjectsState); @@ -154,6 +166,7 @@ describe('Project dispatcher', () => { setBotProjectFile, setRecentProjects, botStates, + botProjectSpaceLoaded, }; }; @@ -161,7 +174,9 @@ describe('Project dispatcher', () => { beforeEach(async () => { navigateTo.mockReset(); - mockProjectResponse = cloneDeep(mockResponse); + mockProjectResponse = cloneDeep(mockProjectData); + mockManifestResponse = cloneDeep(mockManifestData); + mockBotProjectResponse = cloneDeep(mockBotProjectFileData); const rendered: RenderHookResult> = renderRecoilHook( useRecoilTestHook, { @@ -303,7 +318,10 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.boilerplateVersion).toEqual(version); }); - it('should be able to add an existing skill to Botproject and a remote skill', async () => { + it('should be able to add an existing skill to Botproject', async () => { + (httpClient.get as jest.Mock).mockResolvedValueOnce({ + data: {}, + }); const skills = [ { botName: 'Echo-Skill-1', id: '40876.502871204648', location: '/Users/tester/Desktop/Echo-Skill-1' }, { botName: 'Echo-Skill-2', id: '50876.502871204648', location: '/Users/tester/Desktop/Echo-Skill-2' }, @@ -322,7 +340,6 @@ describe('Project dispatcher', () => { (httpClient.put as jest.Mock).mockResolvedValueOnce({ data: mockProjectResponse, }); - await dispatcher.openProject('../test/empty-bot', 'default'); }); @@ -345,6 +362,163 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.botStates.echoSkill2).toBeDefined(); expect(renderedComponent.current.botStates.echoSkill2.botDisplayName).toBe('Echo-Skill-2'); + + await act(async () => { + await dispatcher.addRemoteSkillToBotProject('https://test.net/api/manifest/man', 'test-skill', 'remote'); + }); + + expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); + }); + + it('should be able to add a remote skill to Botproject', async () => { + const mockImplementation = (httpClient.get as jest.Mock).mockImplementation((url: string) => { + if (endsWith(url, '/projects/generateProjectId')) { + return { + data: '1234.1123213', + }; + } else { + return { + data: mockManifestResponse, + }; + } + }); + + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mockProjectResponse, + }); + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + + await act(async () => { + await dispatcher.addRemoteSkillToBotProject( + 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json', + 'one-note', + 'remote' + ); + }); + + expect(renderedComponent.current.botStates.oneNote).toBeDefined(); + expect(renderedComponent.current.botStates.oneNote.botDisplayName).toBe('OneNoteSync'); + expect(renderedComponent.current.botStates.oneNote.location).toBe( + 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json' + ); + expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); + mockImplementation.mockClear(); + }); + + it('should remove a skill from bot project', async () => { + const mockImplementation = (httpClient.get as jest.Mock).mockImplementation((url: string) => { + if (endsWith(url, '/projects/generateProjectId')) { + return { + data: uuid(), + }; + } else { + return { + data: mockManifestResponse, + }; + } + }); + + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mockProjectResponse, + }); + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + + await act(async () => { + await dispatcher.addRemoteSkillToBotProject( + 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json', + 'one-note', + 'remote' + ); + }); + + await act(async () => { + await dispatcher.addRemoteSkillToBotProject( + 'https://test-dev.azurewebsites.net/manifests/onenote-second-manifest.json', + 'one-note-2', + 'remote' + ); + }); + + const oneNoteProjectId = renderedComponent.current.botStates.oneNote.projectId; + mockImplementation.mockClear(); + + await act(async () => { + dispatcher.removeSkillFromBotProject(oneNoteProjectId); + }); + expect(renderedComponent.current.botStates.oneNote).toBeUndefined(); + }); + + it('should be able to add a new skill to Botproject', async () => { + await act(async () => { + (httpClient.put as jest.Mock).mockResolvedValueOnce({ + data: mockProjectResponse, + }); + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + + const newProjectDataClone = cloneDeep(mockProjectResponse); + newProjectDataClone.botName = 'new-bot'; + await act(async () => { + (httpClient.post as jest.Mock).mockResolvedValueOnce({ + data: newProjectDataClone, + }); + await dispatcher.addNewSkillToBotProject({ + name: 'new-bot', + description: '', + schemaUrl: '', + location: '/Users/tester/Desktop/samples', + templateId: 'InterruptionSample', + locale: 'us-en', + qnaKbUrls: [], + }); + }); + + expect(renderedComponent.current.botStates.newBot).toBeDefined(); + expect(renderedComponent.current.botStates.newBot.botDisplayName).toBe('new-bot'); expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); }); + + it('should be able to open a project and its skills in Bot project file', async (done) => { + let callIndex = 0; + (httpClient.put as jest.Mock).mockImplementation(() => { + let mockSkillData: any; + callIndex++; + switch (callIndex) { + case 1: + return Promise.resolve({ data: mockProjectResponse }); + case 2: { + mockSkillData = cloneDeep(mockProjectResponse); + mockSkillData.botName = 'todo-skill'; + mockSkillData.id = '20876.502871204648'; + return Promise.resolve({ data: mockSkillData }); + } + case 3: { + mockSkillData = cloneDeep(mockProjectResponse); + mockSkillData.botName = 'google-keep-sync'; + mockSkillData.id = '50876.502871204648'; + return Promise.resolve({ data: mockSkillData }); + } + } + }); + const matchIndex = findIndex(mockProjectResponse.files, (file: any) => endsWith(file.name, '.botproj')); + mockProjectResponse.files[matchIndex] = { + ...mockProjectResponse.files[matchIndex], + content: JSON.stringify(mockBotProjectResponse), + }; + expect(renderedComponent.current.botProjectSpaceLoaded).toBeFalsy(); + + await act(async () => { + await dispatcher.openProject('../test/empty-bot', 'default'); + }); + setImmediate(() => { + expect(renderedComponent.current.botStates.todoSkill.botDisplayName).toBe('todo-skill'); + expect(renderedComponent.current.botStates.googleKeepSync.botDisplayName).toBe('google-keep-sync'); + expect(renderedComponent.current.botProjectSpaceLoaded).toBeTruthy(); + done(); + }); + }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index 0b50e4c909..7fd85fc959 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -76,20 +76,6 @@ export const botProjectFileDispatcher = () => { } ); - // const renameRootBotInBotProjectFile = useRecoilCallback( - // ({ set, snapshot }: CallbackInterface) => async (projectId: string) => { - // const location = await snapshot.getPromise(locationState(projectId)); - // const botname = await snapshot.getPromise(botNameState(projectId)); - // set(botProjectFileState(projectId), (current: BotProjectSpace) => { - // const result = produce(current, (draftState: BotProjectSpace) => { - // draftState.workspace = convertPathToFileProtocol(location); - // draftState.name = botname; - // }); - // return result; - // }); - // } - // ); - return { addLocalSkillToBotProjectFile, removeSkillFromBotProjectFile, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index 15b98f62d1..e4ec8f5a2e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -14,7 +14,7 @@ import { isEjectRuntimeExistState, filePersistenceState, } from '../atoms/botState'; -import { botEndpointsState, runtimeTemplatesState } from '../atoms'; +import { botEndpointsState } from '../atoms'; import { BotStatus, Text } from './../../constants'; import httpClient from './../../utils/httpUtil'; @@ -218,20 +218,6 @@ export const publisherDispatcher = () => { } ); - const fetchRuntimeTemplates = useRecoilCallback<[], Promise>( - (callbackHelpers: CallbackInterface) => async () => { - const { set } = callbackHelpers; - try { - const response = await httpClient.get(`/runtime/templates`); - if (Array.isArray(response.data)) { - set(runtimeTemplatesState, [...response.data]); - } - } catch (ex) { - // TODO: Handle exceptions - logMessage(callbackHelpers, `Error fetching runtime templates: ${ex}`); - } - } - ); return { getPublishTargetTypes, publishToTarget, @@ -240,6 +226,5 @@ export const publisherDispatcher = () => { getPublishStatus, getPublishHistory, setEjectRuntimeExist, - fetchRuntimeTemplates, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/storage.ts b/Composer/packages/client/src/recoilModel/dispatchers/storage.ts index 6604c6fc77..2c8352e52c 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/storage.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/storage.ts @@ -12,6 +12,7 @@ import { focusedStorageFolderState, applicationErrorState, templateProjectsState, + runtimeTemplatesState, } from '../atoms/appState'; import { FileTypes } from '../../constants'; import { getExtension } from '../../utils/fileUtil'; @@ -151,6 +152,21 @@ export const storageDispatcher = () => { } }); + const fetchRuntimeTemplates = useRecoilCallback<[], Promise>( + (callbackHelpers: CallbackInterface) => async () => { + const { set } = callbackHelpers; + try { + const response = await httpClient.get(`/runtime/templates`); + if (Array.isArray(response.data)) { + set(runtimeTemplatesState, [...response.data]); + } + } catch (ex) { + // TODO: Handle exceptions + logMessage(callbackHelpers, `Error fetching runtime templates: ${ex}`); + } + } + ); + return { fetchStorages, updateCurrentPathForStorage, @@ -161,5 +177,6 @@ export const storageDispatcher = () => { createFolder, updateFolder, fetchTemplates, + fetchRuntimeTemplates, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 81046ae22a..dd6bae0768 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -267,7 +267,6 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any try { schemas.sdk.content = processSchema(projectId, schemas.sdk.content); } catch (err) { - console.log('Error', err); const diagnostics = schemas.diagnostics ?? []; diagnostics.push(err.message); schemas.diagnostics = diagnostics; @@ -454,13 +453,26 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; - const { mappedSkills, endpointsMap } = await mapToRemoteSkill(settings?.skill); - const skillsToAddToBotProjectFile = Object.keys(mappedSkills); - const skills: { [skillId: string]: BotProjectSpaceSkill } = { + let skills: { [skillId: string]: BotProjectSpaceSkill } = { ...currentBotProjectFile.skills, - ...mappedSkills, }; + let skillsToAddToBotProjectFile: string[] = []; + let mappedSkills: { [name: string]: BotProjectSpaceSkill } = {}; + let endpointsMap: Map = new Map(); + + // TODO: Remove flag when UI for Bot Projects is ready + if (process.env.BOT_PROJECT_FLOW) { + const result = await mapToRemoteSkill(settings?.skill); + mappedSkills = result.mappedSkills; + endpointsMap = result.endpointsMap; + skillsToAddToBotProjectFile = Object.keys(mappedSkills); + skills = { + ...skills, + ...mappedSkills, + }; + } + // RootBot loads first + skills load async const totalProjectsCount = Object.keys(skills).length + 1; if (totalProjectsCount > 0) { @@ -477,6 +489,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st skillPromise .then(({ manifestResponse, projectId }) => { addProjectToBotProjectSpace(set, projectId, totalProjectsCount); + // TODO: Remove flag when UI for Bot Projects is ready if (skillsToAddToBotProjectFile.includes(nameIdentifier) && skill.manifest) { const endpointUrl = endpointsMap.get(nameIdentifier) || ''; const endpointName = getEndpointNameGivenUrl(manifestResponse, endpointUrl); @@ -489,7 +502,11 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st ex, }); addProjectToBotProjectSpace(set, projectId, totalProjectsCount); - if (skillsToAddToBotProjectFile.includes(nameIdentifier) && skill.manifest) { + if ( + process.env.BOT_PROJECT_FLOW && + skillsToAddToBotProjectFile.includes(nameIdentifier) && + skill.manifest + ) { dispatcher.addRemoteSkillToBotProjectFile(projectId, skill.manifest, ''); } }); diff --git a/Composer/packages/lib/indexers/src/skillIndexer.ts b/Composer/packages/lib/indexers/src/skillIndexer.ts index 67e71e81b9..326593885b 100644 --- a/Composer/packages/lib/indexers/src/skillIndexer.ts +++ b/Composer/packages/lib/indexers/src/skillIndexer.ts @@ -8,6 +8,7 @@ import toPairs from 'lodash/toPairs'; const index = (skillContent: any[], skillSettings: { [name: string]: SkillSetting } = {}): Skill[] => { return toPairs(skillSettings).map( ([id, settings]): Skill => { + console.log('Content', skillContent); const { content = {} } = skillContent.find(({ id: key }) => key === id) || {}; const { description, endpoints = [] } = content; diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj deleted file mode 100644 index 5aeb3fb004..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.botproj +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", - "name": "bot1", - "workspace": "/Users/srravich/Projects/BotFramework-Composer/Composer/packages/server/src/__mocks__/samplebots/bot1", - "skills": {} -} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog deleted file mode 100644 index 1c7084afda..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/bot1.dialog +++ /dev/null @@ -1,40 +0,0 @@ -{ - "old": "value", - "steps": [ - { - "$kind": "Microsoft.TextInput", - "prompt": "${hello()} I'm a ToDo bot" - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "${bye()} See you again" - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "${bye()} bye bye again" - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "${ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)}" - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "You entered: ${user.date[0].value}" - }, - { - "$kind": "Microsoft.TextInput", - "activity": "${bye3()} See you again" - }, - { - "$kind": "Microsoft.OnIntent", - "intent": "ShowIntent", - "steps": [ - { - "$kind": "Microsoft.BeginDialog", - "dialog": "a" - } - ] - } - ], - "generator": "bot1.lg" -} diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog deleted file mode 100644 index 04ef3aeed1..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/a.dialog +++ /dev/null @@ -1 +0,0 @@ -{"old":"value"} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/knowledge-base/en-us/a.en-us.qna deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-generation/en-us/a.en-us.lg deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/a/language-understanding/en-us/a.en-us.lu deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog deleted file mode 100644 index e6defc9b28..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/b.dialog +++ /dev/null @@ -1,3 +0,0 @@ -{ - "generator": "b.lg" -} \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/knowledge-base/en-us/b.en-us.qna deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-generation/en-us/b.en-us.lg deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/b/language-understanding/en-us/b.en-us.lu deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/dialogs/root/knowledge-base/en-us/root.en-us.qna deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna b/Composer/packages/server/src/__mocks__/samplebots/copy/knowledge-base/en-us/bot1.en-us.qna deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/bot1.en-us.lg deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg deleted file mode 100644 index 08f7572efa..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/language-generation/en-us/common.en-us.lg +++ /dev/null @@ -1,14 +0,0 @@ -# greet - - Hello! - -# hello - - Hello Welcome! - -# bye - - Good bye! - -# ShowImage(contentUrl, contentType) -- ``` -You sent: -[attachment=@{contentUrl} @{contentType}] -``` \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu deleted file mode 100644 index df74df7d1e..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/language-understanding/en-us/bot1.en-us.lu +++ /dev/null @@ -1,2 +0,0 @@ -# Greeting --hi \ No newline at end of file diff --git a/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json b/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json deleted file mode 100644 index e468dd1746..0000000000 --- a/Composer/packages/server/src/__mocks__/samplebots/copy/settings/appsettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "MicrosoftAppId": "", - "luis": { - "name": "", - "authoringRegion": "westus", - "defaultLanguage": "en-us", - "environment": "composer", - "endpoint": "", - "authoringEndpoint": "", - "authoringKey": "", - "endpointKey": "" - }, - "qna": { - "knowledgebaseid": "", - "hostname": "", - "endpointKey": "", - "subscriptionKey": "" - }, - "downsampling": { - "maxImbalanceRatio": 10, - "maxUtteranceAllowed": 15000 - }, - "defaultLanguage": "en-us", - "languages": [ - "en-us" - ], - "MicrosoftAppPassword": "" -} \ No newline at end of file diff --git a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts index 9fcfc8ccb4..f11fff0a2d 100644 --- a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts @@ -68,7 +68,7 @@ describe('get types', () => { describe('status', () => { const target = 'default'; - it.only('should get status', async () => { + fit('should get status', async () => { const projectId = await BotProjectService.openProject(location2); const mockReq = { @@ -115,6 +115,7 @@ describe('rollback', () => { body: {}, } as Request; await PublishController.rollback(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(400); }); }); 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 6dcf62c385..d76d70c212 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -99,11 +99,11 @@ describe('copyTo', () => { } }); - it('should copy successfully', async () => { + fit('should copy successfully', async () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); - expect(project.files.length).toBe(13); + expect(project.files.length).toBe(15); }); }); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index b4e3e03259..5731702c57 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -108,7 +108,6 @@ export const serializeFiles = async (fileStorage, rootPath, botName) => { templateInterpolate(BotStructureTemplate.lu, { LOCALE: '*', BOTNAME: '*' }), templateInterpolate(BotStructureTemplate.qna, { LOCALE: '*', BOTNAME: '*' }), templateInterpolate(BotStructureTemplate.dialogSchema, { BOTNAME: '*' }), - templateInterpolate(BotStructureTemplate.dialogSchema, { BOTNAME: '*' }), templateInterpolate(BotStructureTemplate.botProject, { BOTNAME: '*' }), ]; for (const pattern of entryPatterns) { diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index 34156e889f..0e00d2a9c1 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -29,7 +29,6 @@ router.put('/projects/:projectId/files/:name', ProjectController.updateFile); router.delete('/projects/:projectId/files/:name', ProjectController.removeFile); router.post('/projects/:projectId/files', ProjectController.createFile); router.get('/projects/:projectId/skill/retrieveSkillManifest', ProjectController.getSkill); -router.get('/projects/skill/retrieveSkillManifest', 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); From 7e4f5d0e15236f02e03a0297bb80a2561dcba66d Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 5 Oct 2020 23:08:08 -0700 Subject: [PATCH 021/100] Removed code for bot migration Signed-off-by: Srinaath Ravichandran --- .../recoilModel/dispatchers/utils/project.ts | 38 ++----------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index dd6bae0768..50af380641 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -440,12 +440,11 @@ const mapToRemoteSkill = async (skillsSettings: { const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { const { projectData, botFiles } = data; - const { set, snapshot } = callbackHelpers; - const dispatcher = await snapshot.getPromise(dispatcherState); + const { set } = callbackHelpers; const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); const rootBotProjectId = projectData.id; - const { name, settings } = projectData; + const { name } = projectData; set(botNameIdentifierState(rootBotProjectId), camelCase(name)); if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { @@ -453,26 +452,10 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; - let skills: { [skillId: string]: BotProjectSpaceSkill } = { + const skills: { [skillId: string]: BotProjectSpaceSkill } = { ...currentBotProjectFile.skills, }; - let skillsToAddToBotProjectFile: string[] = []; - let mappedSkills: { [name: string]: BotProjectSpaceSkill } = {}; - let endpointsMap: Map = new Map(); - - // TODO: Remove flag when UI for Bot Projects is ready - if (process.env.BOT_PROJECT_FLOW) { - const result = await mapToRemoteSkill(settings?.skill); - mappedSkills = result.mappedSkills; - endpointsMap = result.endpointsMap; - skillsToAddToBotProjectFile = Object.keys(mappedSkills); - skills = { - ...skills, - ...mappedSkills, - }; - } - // RootBot loads first + skills load async const totalProjectsCount = Object.keys(skills).length + 1; if (totalProjectsCount > 0) { @@ -487,14 +470,8 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st } if (skillPromise) { skillPromise - .then(({ manifestResponse, projectId }) => { + .then(({ projectId }) => { addProjectToBotProjectSpace(set, projectId, totalProjectsCount); - // TODO: Remove flag when UI for Bot Projects is ready - if (skillsToAddToBotProjectFile.includes(nameIdentifier) && skill.manifest) { - const endpointUrl = endpointsMap.get(nameIdentifier) || ''; - const endpointName = getEndpointNameGivenUrl(manifestResponse, endpointUrl); - dispatcher.addRemoteSkillToBotProjectFile(projectId, skill.manifest, endpointName); - } }) .catch((ex) => { const { projectId } = handleSkillLoadingFailure(callbackHelpers, { @@ -502,13 +479,6 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st ex, }); addProjectToBotProjectSpace(set, projectId, totalProjectsCount); - if ( - process.env.BOT_PROJECT_FLOW && - skillsToAddToBotProjectFile.includes(nameIdentifier) && - skill.manifest - ) { - dispatcher.addRemoteSkillToBotProjectFile(projectId, skill.manifest, ''); - } }); } } From 493c604dafee0f9eca5e083f7e62efa8bf4cce9e Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 00:03:20 -0700 Subject: [PATCH 022/100] Server side tests added Signed-off-by: Srinaath Ravichandran --- .../dispatchers/__tests__/project.test.tsx | 1 - .../recoilModel/dispatchers/utils/project.ts | 23 ---------------- .../packages/lib/indexers/src/skillIndexer.ts | 1 - .../controllers/__tests__/publisher.test.ts | 2 +- .../models/bot/__tests__/botProject.test.ts | 26 ++++++++++++++++--- 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index b6d3570cba..da7ac2075f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -8,7 +8,6 @@ import { useRecoilState } from 'recoil'; import cloneDeep from 'lodash/cloneDeep'; import endsWith from 'lodash/endsWith'; import findIndex from 'lodash/findIndex'; -import { SkillSetting } from '@bfc/shared/src/types'; import httpClient from '../../../utils/httpUtil'; import { projectDispatcher } from '../project'; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 50af380641..9cb350ab96 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -12,9 +12,7 @@ import { QnAFile, BotProjectSpace, BotProjectFile, - SkillSetting, BotProjectSpaceSkill, - getEndpointNameGivenUrl, } from '@bfc/shared'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; @@ -71,7 +69,6 @@ import { import { undoHistoryState } from '../../undo/history'; import { rootBotProjectIdSelector } from '../../selectors'; import { trimFileProtocol, getUniqueName } from '../../../utils/fileUtil'; -import { dispatcherState } from '../../DispatcherWrapper'; export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); @@ -418,26 +415,6 @@ const handleSkillLoadingFailure = (callbackHelpers, { ex, skillNameIdentifier }) return { projectId: projectId }; }; -const mapToRemoteSkill = async (skillsSettings: { - [skillName: string]: SkillSetting; -}): Promise<{ mappedSkills: { [name: string]: BotProjectSpaceSkill }; endpointsMap: Map }> => { - const endpointsMap = new Map(); - try { - const mappedSkills: { [name: string]: BotProjectSpaceSkill } = {}; - for (const skillName in skillsSettings) { - const val = skillsSettings[skillName]; - mappedSkills[skillName] = { - manifest: val.manifestUrl || '', - remote: true, - }; - endpointsMap.set(skillName, val.endpointUrl || ''); - } - return { mappedSkills, endpointsMap }; - } catch (ex) { - return { mappedSkills: {}, endpointsMap: endpointsMap }; - } -}; - const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { const { projectData, botFiles } = data; const { set } = callbackHelpers; diff --git a/Composer/packages/lib/indexers/src/skillIndexer.ts b/Composer/packages/lib/indexers/src/skillIndexer.ts index 326593885b..67e71e81b9 100644 --- a/Composer/packages/lib/indexers/src/skillIndexer.ts +++ b/Composer/packages/lib/indexers/src/skillIndexer.ts @@ -8,7 +8,6 @@ import toPairs from 'lodash/toPairs'; const index = (skillContent: any[], skillSettings: { [name: string]: SkillSetting } = {}): Skill[] => { return toPairs(skillSettings).map( ([id, settings]): Skill => { - console.log('Content', skillContent); const { content = {} } = skillContent.find(({ id: key }) => key === id) || {}; const { description, endpoints = [] } = content; diff --git a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts index f11fff0a2d..a59cf595c8 100644 --- a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts @@ -68,7 +68,7 @@ describe('get types', () => { describe('status', () => { const target = 'default'; - fit('should get status', async () => { + it('should get status', async () => { const projectId = await BotProjectService.openProject(location2); const mockReq = { 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 d76d70c212..1b5b6a597d 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -5,6 +5,7 @@ import fs from 'fs'; import rimraf from 'rimraf'; import { DialogFactory, SDKKinds } from '@bfc/shared'; +import { endsWith } from 'lodash'; import { Path } from '../../../utility/path'; import { BotProject } from '../botProject'; @@ -14,6 +15,19 @@ jest.mock('azure-storage', () => { return {}; }); +jest.mock('../../../services/asset', () => { + return { + manager: { + botProjectFileTemplate: { + $schema: '', + name: '', + workspace: '', + skills: {}, + }, + }, + }; +}); + const botDir = '../../../__mocks__/samplebots/bot1'; const mockLocationRef: LocationRef = { @@ -30,7 +44,13 @@ beforeEach(async () => { describe('init', () => { it('should get project successfully', () => { const project: { [key: string]: any } = proj.getProject(); - expect(project.files.length).toBe(13); + expect(project.files.length).toBe(15); + }); + + it('should always have a default bot project file', () => { + const project: { [key: string]: any } = proj.getProject(); + const botprojectFile = project.files.find((file) => endsWith(file.name, 'botproj')); + expect(botprojectFile).toBeDefined(); }); }); @@ -99,7 +119,7 @@ describe('copyTo', () => { } }); - fit('should copy successfully', async () => { + it('should copy successfully', async () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); @@ -380,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(14); + expect(project.files.length).toBe(15); await newBotProject.deleteAllFiles(); expect(fs.existsSync(copyDir)).toBe(false); }); From 12648f714bedf00f86699e2f7be51337e27904e8 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 00:11:42 -0700 Subject: [PATCH 023/100] Updated schema url to the correct one Signed-off-by: Srinaath Ravichandran --- .../samples/assets/projects/ActionsSample/actionssample.botproj | 2 +- .../AskingQuestionsSample/askingquestionssample.botproj | 2 +- .../controllingconversationflowsample.botproj | 2 +- .../plugins/samples/assets/projects/EmptyBot/emptybot.botproj | 2 +- .../projects/InterruptionSample/interruptionssample.botproj | 2 +- .../projects/QnAMakerLUISSample/qnamakerluissample.botproj | 2 +- .../plugins/samples/assets/projects/QnASample/qnasample.botproj | 2 +- .../RespondingWithCardsSample/respondingwithcardssample.botproj | 2 +- .../RespondingWithTextSample/respondingwithtextsample.botproj | 2 +- .../ToDoBotWithLuisSample/todobotwithluissample.botproj | 2 +- .../samples/assets/projects/TodoSample/todosample.botproj | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj +++ b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj +++ b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj +++ b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj +++ b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj +++ b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj +++ b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj +++ b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj +++ b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj +++ b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj +++ b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} diff --git a/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj index 3ca0f8a01a..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj +++ b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/botproject.schema", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} From fc707fe9b56d750717c8e08bec0e9f2fa316b3cc Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 00:54:35 -0700 Subject: [PATCH 024/100] Fix lint and update schema url on echobot Signed-off-by: Srinaath Ravichandran --- Composer/packages/client/src/recoilModel/dispatchers/project.ts | 2 +- .../packages/server/src/models/bot/__tests__/botProject.test.ts | 2 +- .../plugins/samples/assets/projects/EchoBot/echobot.botproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 65e469bbcb..e14a503c9f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -240,7 +240,7 @@ export const projectDispatcher = () => { isRemote: false, }); projectIdCache.set(projectId); - navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls); + navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls, templateId); } catch (ex) { set(botProjectIdsState, []); handleProjectFailure(callbackHelpers, ex); 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 1b5b6a597d..69ae7de469 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import rimraf from 'rimraf'; import { DialogFactory, SDKKinds } from '@bfc/shared'; -import { endsWith } from 'lodash'; +import endsWith from 'lodash/endsWith'; import { Path } from '../../../utility/path'; import { BotProject } from '../botProject'; diff --git a/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj index 17932761e7..543cc3b976 100644 --- a/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj +++ b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", "workspace": "", "skills": {} From 729fff3812e312b295c6b0d08650c8a68bb53531 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 11:31:31 -0700 Subject: [PATCH 025/100] botstates Signed-off-by: Srinaath Ravichandran More file util tests added Signed-off-by: Srinaath Ravichandran Support for exporting .botproj file Signed-off-by: Srinaath Ravichandran --- .../client/src/recoilModel/atoms/botState.ts | 11 ++------ .../__tests__/botProjectFile.test.tsx | 27 ++---------------- .../recoilModel/dispatchers/botProjectFile.ts | 13 ++++----- .../recoilModel/dispatchers/utils/project.ts | 7 +++-- .../src/utils/__test__/fileUtil.test.ts | 11 ++++++++ .../packages/client/src/utils/fileUtil.ts | 23 ++++----------- .../shared/__tests__/fileUtils/index.test.ts | 28 +++++++++++++++++++ .../lib/shared/src/fileUtils/index.ts | 22 +++++++++++++++ Composer/packages/lib/shared/src/index.ts | 1 + .../server/src/models/bot/botProject.ts | 15 ++++++++-- .../src/models/storage/localDiskStorage.ts | 2 +- 11 files changed, 95 insertions(+), 65 deletions(-) create mode 100644 Composer/packages/client/src/utils/__test__/fileUtil.test.ts create mode 100644 Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts create mode 100644 Composer/packages/lib/shared/src/fileUtils/index.ts diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index dda741b498..4c50533561 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -261,7 +261,7 @@ export const filePersistenceState = atomFamily({ }); export const botProjectFileState = atomFamily({ - key: getFullyQualifiedKey('botProjectFileState'), + key: getFullyQualifiedKey('botProjectFile'), default: { content: {} as BotProjectSpace, id: '', @@ -269,18 +269,13 @@ export const botProjectFileState = atomFamily({ }, }); -// Always using the first manifest file for now. We dont have the need to support multiple manifest for a skill -export const currentManifestIndexState = atomFamily({ - key: getFullyQualifiedKey('currentManifestIndexState'), - default: 0, -}); - export const botErrorState = atomFamily({ key: getFullyQualifiedKey('botError'), default: undefined, }); +// Object key to identify the skill in BotProject file and settings.skill export const botNameIdentifierState = atomFamily({ - key: getFullyQualifiedKey('botNameIdentifierState'), + key: getFullyQualifiedKey('botNameIdentifier'), default: '', }); 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 411f7effd1..df4050b152 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx @@ -3,6 +3,7 @@ import { selector, useRecoilValue, selectorFamily, useRecoilState } from 'recoil'; import { act, RenderHookResult, HookResult } from '@bfc/test-utils/lib/hooks'; +import noop from 'lodash/noop'; import { botProjectFileDispatcher } from '../botProjectFile'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; @@ -26,11 +27,7 @@ const testSkillId = '123.1sd23'; describe('Bot Project File dispatcher', () => { const skillsDataSelector = selectorFamily({ key: 'skillsDataSelector-botProjectFile', - get: (skillId: string) => ({ get }) => { - return { - name: get(botNameIdentifierState(skillId)), - }; - }, + get: (skillId: string) => noop, set: (skillId: string) => ({ set }, stateUpdater: any) => { const { botNameIdentifier, location } = stateUpdater; set(botNameIdentifierState(skillId), botNameIdentifier); @@ -128,7 +125,7 @@ describe('Bot Project File dispatcher', () => { }); expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toBe( - 'file://Users/tester/Desktop/LoadedBotProject/Todo-Skill' + 'file:///Users/tester/Desktop/LoadedBotProject/Todo-Skill' ); expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.remote).toBeFalsy(); }); @@ -151,24 +148,6 @@ describe('Bot Project File dispatcher', () => { expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.endpointName).toBe('remote'); }); - it('should add a remote skill to bot project file', async () => { - const manifestUrl = 'https://test-dev.azurewebsites.net/manifests/test-2-1-preview-1-manifest.json'; - await act(async () => { - renderedComponent.current.setSkillsData({ - location: manifestUrl, - botNameIdentifier: 'oneNoteSkill', - }); - }); - - await act(async () => { - dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); - }); - - expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); - expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.workspace).toBeUndefined(); - expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.endpointName).toBe('remote'); - }); - it('should remove a skill from the bot project file', async () => { const manifestUrl = 'https://test-dev.azurewebsites.net/manifests/test-2-1-preview-1-manifest.json'; await act(async () => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index d7b5461236..ad523b1498 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -4,11 +4,10 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpaceSkill } from '@bfc/shared'; +import { BotProjectSpaceSkill, convertAbsolutePathToFileProtocol } from '@bfc/shared'; import { botNameIdentifierState, botProjectFileState, locationState } from '../atoms'; -import { isBotProjectSpaceSelector, rootBotProjectIdSelector } from '../selectors'; -import { convertPathToFileProtocol } from '../../utils/fileUtil'; +import { rootBotProjectIdSelector } from '../selectors'; export const botProjectFileDispatcher = () => { const addLocalSkillToBotProjectFile = useRecoilCallback( @@ -23,7 +22,7 @@ export const botProjectFileDispatcher = () => { set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { const skill: BotProjectSpaceSkill = { - workspace: convertPathToFileProtocol(skillLocation), + workspace: convertAbsolutePathToFileProtocol(skillLocation), remote: false, }; draftState.content.skills[botName] = skill; @@ -35,9 +34,8 @@ export const botProjectFileDispatcher = () => { const addRemoteSkillToBotProjectFile = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async (skillId: string, manifestUrl: string, endpointName: string) => { - const isBotProjectSpace = await snapshot.getPromise(isBotProjectSpaceSelector); const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); - if (!isBotProjectSpace || !rootBotProjectId) { + if (!rootBotProjectId) { return; } const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); @@ -59,9 +57,8 @@ export const botProjectFileDispatcher = () => { const removeSkillFromBotProjectFile = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { - const isBotProjectSpace = await snapshot.getPromise(isBotProjectSpaceSelector); const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); - if (!isBotProjectSpace || !rootBotProjectId) { + if (!rootBotProjectId) { return; } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 9cb350ab96..207d9d5cee 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -13,6 +13,7 @@ import { BotProjectSpace, BotProjectFile, BotProjectSpaceSkill, + convertFileProtocolToPath, } from '@bfc/shared'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; @@ -68,7 +69,7 @@ import { } from '../../atoms'; import { undoHistoryState } from '../../undo/history'; import { rootBotProjectIdSelector } from '../../selectors'; -import { trimFileProtocol, getUniqueName } from '../../../utils/fileUtil'; +import { getUniqueName } from '../../../utils/fileUtil'; export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); @@ -440,7 +441,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const skill = skills[nameIdentifier]; let skillPromise; if (!skill.remote && skill.workspace) { - const skillPath = trimFileProtocol(skill.workspace); + const skillPath = convertFileProtocolToPath(skill.workspace); skillPromise = openLocalSkill(callbackHelpers, skillPath, storageId, nameIdentifier); } else if (skill.manifest) { skillPromise = openRemoteSkill(callbackHelpers, skill.manifest, nameIdentifier); @@ -537,7 +538,7 @@ export const checkIfBotExistsInBotProjectFile = async ( } } else { if (workspace) { - const resolvedPath = trimFileProtocol(workspace); + const resolvedPath = convertFileProtocolToPath(workspace); if (pathOrManifest === resolvedPath) { return true; } diff --git a/Composer/packages/client/src/utils/__test__/fileUtil.test.ts b/Composer/packages/client/src/utils/__test__/fileUtil.test.ts new file mode 100644 index 0000000000..fe5e476715 --- /dev/null +++ b/Composer/packages/client/src/utils/__test__/fileUtil.test.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getUniqueName } from '../fileUtil'; + +describe('File utils', () => { + it('should get a unique name', () => { + const uniqueName = getUniqueName(['test', 'test-1', 'test-2', 'test-3'], 'test'); + expect(uniqueName).toBe('test-4'); + }); +}); diff --git a/Composer/packages/client/src/utils/fileUtil.ts b/Composer/packages/client/src/utils/fileUtil.ts index d2b1360c4f..6fadad98d1 100644 --- a/Composer/packages/client/src/utils/fileUtil.ts +++ b/Composer/packages/client/src/utils/fileUtil.ts @@ -106,25 +106,12 @@ export async function loadLocale(locale: string) { } } -export const trimFileProtocol = (path: string) => { - if (!path) { - return ''; - } - return path.replace('file://', ''); -}; - -export const convertPathToFileProtocol = (path: string) => { - if (!path) { - return ''; - } - return `file://${path}`; -}; - -export const getUniqueName = (list: string[], currentName: string) => { +export const getUniqueName = (list: string[], currentName: string, seperator = '-') => { let uniqueName = currentName; - const i = 1; - while (list.includes(currentName)) { - uniqueName += `-${i}`; + let i = 1; + while (list.includes(uniqueName)) { + uniqueName = `${currentName}${seperator}${i}`; + i++; } return uniqueName; }; diff --git a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts new file mode 100644 index 0000000000..124c0e62cf --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { convertAbsolutePathToFileProtocol, convertFileProtocolToPath } from '../../src/fileUtils'; + +it('should convert a posix path to file protocol', () => { + const testPath = '/Users/tester/empty-bot-0'; + expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///Users/tester/empty-bot-0'); +}); + +it('should convert a windows path to file protocol', () => { + const testPath = 'C:/Users/Tester/empty-bot-0'; + expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///C:/Users/Tester/empty-bot-0'); +}); + +it('should convert a Windows file protocol path to regular path', () => { + const testPath = 'file:///C:/Users/Tester/empty-bot-0'; + expect(convertFileProtocolToPath(testPath)).toBe('C:/Users/Tester/empty-bot-0'); +}); + +it('should convert a Mac file protocol path to regular path', () => { + const testPath = 'file:///users/tester/empty-bot-0'; + expect(convertFileProtocolToPath(testPath)).toBe('/users/tester/empty-bot-0'); +}); + +it('should give empty string if path is not available', () => { + const testPath = ''; + expect(convertFileProtocolToPath(testPath)).toBe(''); +}); diff --git a/Composer/packages/lib/shared/src/fileUtils/index.ts b/Composer/packages/lib/shared/src/fileUtils/index.ts new file mode 100644 index 0000000000..0cb4cf7829 --- /dev/null +++ b/Composer/packages/lib/shared/src/fileUtils/index.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import trimStart from 'lodash/trimStart'; + +export const convertFileProtocolToPath = (pathToBot: string): string => { + const fileProtocolRemoved = pathToBot.replace('file://', ''); + if (fileProtocolRemoved.match(/^\/[a-zA-Z]:\//g)) { + //Windows path with file protocol. Remove leading / + return trimStart(fileProtocolRemoved, '/'); + } + return fileProtocolRemoved; +}; + +export const convertAbsolutePathToFileProtocol = (pathToBot: string): string => { + let pathName = pathToBot.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash + if (pathName[0] !== '/') { + pathName = '/' + pathName; + } + return encodeURI('file://' + pathName); +}; diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index 0b726ece36..b3688c4e7a 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -22,4 +22,5 @@ export * from './types'; export * from './viewUtils'; export * from './walkerUtils'; export * from './skillsUtils'; +export * from './fileUtils'; export const DialogUtils = dialogUtils; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 6accb5d219..1e79db6aea 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -6,7 +6,16 @@ import fs from 'fs'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; -import { getNewDesigner, FileInfo, Skill, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; +import { + getNewDesigner, + FileInfo, + Skill, + Diagnostic, + IBotProject, + DialogSetting, + FileExtensions, + convertAbsolutePathToFileProtocol, +} from '@bfc/shared'; import merge from 'lodash/merge'; import { UserIdentity, ExtensionContext } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; @@ -352,7 +361,7 @@ export class BotProject implements IBotProject { for (const botProjectFile of this.botProjectFiles) { const { relativePath } = botProjectFile; const content = JSON.parse(botProjectFile.content); - content.workspace = this.dataDir; + content.workspace = convertAbsolutePathToFileProtocol(this.dataDir); content.name = botName; await this._updateFile(relativePath, JSON.stringify(content, null, 2)); } @@ -793,7 +802,7 @@ export class BotProject implements IBotProject { const fileName = `${this.name}${FileExtensions.BotProject}`; const root = this.dataDir; - defaultBotProjectFile.workspace = root; + defaultBotProjectFile.workspace = convertAbsolutePathToFileProtocol(root); defaultBotProjectFile.name = this.name; await this._createFile(fileName, JSON.stringify(defaultBotProjectFile, null, 2)); diff --git a/Composer/packages/server/src/models/storage/localDiskStorage.ts b/Composer/packages/server/src/models/storage/localDiskStorage.ts index bd49659a05..441ba5603c 100644 --- a/Composer/packages/server/src/models/storage/localDiskStorage.ts +++ b/Composer/packages/server/src/models/storage/localDiskStorage.ts @@ -109,7 +109,7 @@ export class LocalDiskStorage implements IFileStorage { archive.directory(directory, directory.split(source)[1]); }); - const files = await glob('*.dialog', { cwd: source, dot: true }); + const files = await glob(['*.dialog', '*.botproj'], { cwd: source, dot: true }); files.forEach((file) => { archive.file(path.format({ dir: `${source}/`, base: `${file}` }), { name: path.basename(file) }); }); From 103c7b7009ff15a0961b41995b390e8f50d1d41a Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 22:52:59 -0700 Subject: [PATCH 026/100] Clean up paths Signed-off-by: Srinaath Ravichandran --- .../server/src/__mocks__/samplebots/bot1/bot1.botproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj b/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj index 5aeb3fb004..2827aeb871 100644 --- a/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj +++ b/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.botproj @@ -1,6 +1,6 @@ { "$schema": "https://schemas.botframework.com/schemas/botprojects/v0.1/botproject-schema.json", "name": "bot1", - "workspace": "/Users/srravich/Projects/BotFramework-Composer/Composer/packages/server/src/__mocks__/samplebots/bot1", + "workspace": "file:///Users/tester/Projects/BotFramework-Composer/Composer/packages/server/src/__mocks__/samplebots/bot1", "skills": {} -} \ No newline at end of file +} From 21ce792aa6434eb9c961ab11de81615b033abf2a Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 23:28:54 -0700 Subject: [PATCH 027/100] Spellings cleanup Signed-off-by: Srinaath Ravichandran --- .../client/src/recoilModel/dispatchers/project.ts | 2 ++ .../src/recoilModel/dispatchers/utils/project.ts | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index e14a503c9f..c5673b3ba0 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -21,6 +21,7 @@ import { currentProjectIdState, botErrorState, botNameIdentifierState, + botProjectSpaceLoadedState, } from '../atoms'; import { dispatcherState } from '../DispatcherWrapper'; import { getFileNameFromPath } from '../../utils/fileUtil'; @@ -291,6 +292,7 @@ export const projectDispatcher = () => { resetBotStates(callbackHelpers, projectId); reset(botProjectIdsState); reset(currentProjectIdState); + reset(botProjectSpaceLoadedState); } catch (e) { logMessage(callbackHelpers, e.message); } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 207d9d5cee..5e68e76545 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -395,11 +395,11 @@ export const createNewBotFromTemplate = async ( const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => { let isBotProjectLoaded = false; set(botProjectIdsState, (current: string[]) => { - const botProjectIDs = [...current, projectId]; - if (botProjectIDs.length === skillCt) { + const botProjectIds = [...current, projectId]; + if (botProjectIds.length === skillCt) { isBotProjectLoaded = true; } - return botProjectIDs; + return botProjectIds; }); if (isBotProjectLoaded) { set(botProjectSpaceLoadedState, true); @@ -413,7 +413,7 @@ const handleSkillLoadingFailure = (callbackHelpers, { ex, skillNameIdentifier }) set(botDisplayNameState(projectId), skillNameIdentifier); set(botNameIdentifierState(projectId), skillNameIdentifier); setErrorOnBotProject(callbackHelpers, projectId, skillNameIdentifier, ex); - return { projectId: projectId }; + return projectId; }; const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { @@ -452,7 +452,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st addProjectToBotProjectSpace(set, projectId, totalProjectsCount); }) .catch((ex) => { - const { projectId } = handleSkillLoadingFailure(callbackHelpers, { + const projectId = handleSkillLoadingFailure(callbackHelpers, { skillNameIdentifier: nameIdentifier, ex, }); From c56e1b18ee8e4bd4a535c28cbbe29b2186912413 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 23:37:43 -0700 Subject: [PATCH 028/100] Updated gitignore Signed-off-by: Srinaath Ravichandran --- Composer/packages/server/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/Composer/packages/server/.gitignore b/Composer/packages/server/.gitignore index aeeb3279c8..ea8a2d16ff 100644 --- a/Composer/packages/server/.gitignore +++ b/Composer/packages/server/.gitignore @@ -5,3 +5,4 @@ schemas/*-pseudo.uischema src/__tests__/__data__.json src/locales/*-pseudo.json extensions.json +!packages/server/schemas/botproject.schema From b78d118f4847420cc9a614efb098636838db28b2 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 6 Oct 2020 23:39:10 -0700 Subject: [PATCH 029/100] Typo fixed Signed-off-by: Srinaath Ravichandran --- Composer/packages/server/schemas/botproject.schema | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema index 745c54f133..4decf4b2cf 100644 --- a/Composer/packages/server/schemas/botproject.schema +++ b/Composer/packages/server/schemas/botproject.schema @@ -18,7 +18,7 @@ }, "workspace": { "type": "string", - "description": "Path to the Root Bot. If a workspace is local, we use the file protocol as opposed to http/https for remote workspaces." + "description": "Absolute path to the Root Bot. If a workspace is local, we use the file protocol as opposed to http/https for remote workspaces." }, "skills": { "description": "List of skills (remote or local) skills that the Root Bot consumes.", @@ -56,7 +56,7 @@ }, "workspace": { "type": "string", - "description": "Path to a skill workspace. If a workspace is local, we use the file protocol as opposed to http/https protocols for remote workspaces." + "description": "Absolute path to a skill workspace. If a workspace is local, we use the file protocol as opposed to http/https protocols for remote workspaces." }, "remote": { "description": "Indication if the skill is remote or local skill.", From de1a41406a0f298c31ff48aa651d8145453f59cf Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 7 Oct 2020 13:57:27 -0700 Subject: [PATCH 030/100] Set project ignore validation to true while opening remote skill Signed-off-by: Srinaath Ravichandran --- .../client/src/recoilModel/dispatchers/utils/project.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 5e68e76545..4a1718dbe9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -331,7 +331,9 @@ export const openRemoteSkill = async ( const stringified = stringify({ url: manifestUrl, }); - const manifestResponse = await httpClient.get(`/projects/${projectId}/skill/retrieveSkillManifest?${stringified}`); + const manifestResponse = await httpClient.get( + `/projects/${projectId}/skill/retrieveSkillManifest?${stringified}&ignoreProjectValidation=true` + ); set(projectMetaDataState(projectId), { isRootBot: false, isRemote: true, From 5576163afcf7569de850981e50081d89badd128a Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Thu, 8 Oct 2020 15:17:07 -0700 Subject: [PATCH 031/100] Following sdk.schema approach to specificy object type using ref Signed-off-by: Srinaath Ravichandran --- Composer/packages/server/schemas/botproject.schema | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema index 4decf4b2cf..304d559cb6 100644 --- a/Composer/packages/server/schemas/botproject.schema +++ b/Composer/packages/server/schemas/botproject.schema @@ -24,11 +24,9 @@ "description": "List of skills (remote or local) skills that the Root Bot consumes.", "type": "object", "patternProperties": { - "^.*$": { - "$ref": "#/definitions/skill" - } + "^\\$": { "title": "Skill name", "description": "Unique Skill name identifier" } }, - "additionalProperties": false + "additionalProperties": { "$ref": "#/definitions/skill" } } }, "definitions": { From 1d815f12d737bbcad60e360700fa351bd7294443 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 9 Oct 2020 10:23:46 -0700 Subject: [PATCH 032/100] Runtime start first pass Signed-off-by: Srinaath Ravichandran skills update Signed-off-by: Srinaath Ravichandran Migration of old bot wrapped up Signed-off-by: Srinaath Ravichandran --- .../__tests__/components/skill.test.tsx | 4 +- .../src/components/CreateSkillModal.tsx | 32 +--- .../components/Modal/DisplayManifestModal.tsx | 2 +- .../TestController/TestController.tsx | 12 +- .../TestController/startBotsDialog.tsx | 170 ++++++++++++++++++ .../client/src/pages/design/DesignPage.tsx | 29 ++- .../exportSkillModal/generateSkillManifest.ts | 4 +- .../client/src/pages/skills/skill-list.tsx | 43 +++-- .../client/src/recoilModel/atoms/botState.ts | 17 +- .../dispatchers/__tests__/project.test.tsx | 1 - .../dispatchers/__tests__/skill.test.ts | 3 +- .../recoilModel/dispatchers/botProjectFile.ts | 142 ++++++++++++--- .../src/recoilModel/dispatchers/project.ts | 2 +- .../src/recoilModel/dispatchers/setting.ts | 36 +--- .../src/recoilModel/dispatchers/skill.ts | 110 ++++-------- .../recoilModel/dispatchers/utils/project.ts | 74 ++++++-- .../persistence/FilePersistence.ts | 1 + .../client/src/recoilModel/selectors/index.ts | 1 + .../src/recoilModel/selectors/project.ts | 11 ++ .../src/recoilModel/selectors/skills.ts | 45 +++++ .../packages/client/src/shell/useShell.ts | 12 +- Composer/packages/lib/indexers/src/index.ts | 6 +- .../lib/indexers/src/skillManifestIndexer.ts | 6 +- .../lib/shared/src/skillsUtils/index.ts | 23 ++- .../server/src/models/bot/botProject.ts | 7 - .../server/src/models/bot/skillManager.ts | 27 --- Composer/packages/types/src/indexers.ts | 31 ++-- Composer/packages/types/src/server.ts | 4 +- Composer/packages/types/src/settings.ts | 2 - Composer/packages/types/src/shell.ts | 6 +- .../src/SelectSkillDialogField.tsx | 42 ++--- .../src/SkillEndpointField.tsx | 67 ++++--- .../src/__tests__/SelectSkillDialog.test.tsx | 7 +- Composer/plugins/azurePublish/yarn.lock | 54 +++++- 34 files changed, 693 insertions(+), 340 deletions(-) create mode 100644 Composer/packages/client/src/components/TestController/startBotsDialog.tsx create mode 100644 Composer/packages/client/src/recoilModel/selectors/skills.ts diff --git a/Composer/packages/client/__tests__/components/skill.test.tsx b/Composer/packages/client/__tests__/components/skill.test.tsx index f25044478a..cf13d9c49e 100644 --- a/Composer/packages/client/__tests__/components/skill.test.tsx +++ b/Composer/packages/client/__tests__/components/skill.test.tsx @@ -23,7 +23,7 @@ jest.mock('../../src/components/Modal/dialogStyle', () => ({})); const skills: Skill[] = [ { id: 'email-skill', - content: {}, + manifestContent: {}, 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.', @@ -33,7 +33,7 @@ const skills: Skill[] = [ }, { id: 'point-of-interest-skill', - content: {}, + manifestContent: {}, 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.', diff --git a/Composer/packages/client/src/components/CreateSkillModal.tsx b/Composer/packages/client/src/components/CreateSkillModal.tsx index 0a900a564e..6465e2816a 100644 --- a/Composer/packages/client/src/components/CreateSkillModal.tsx +++ b/Composer/packages/client/src/components/CreateSkillModal.tsx @@ -11,11 +11,10 @@ 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 { SkillSetting } from '@bfc/shared'; import { addSkillDialog } from '../constants'; import httpClient from '../utils/httpUtil'; -import { skillsState } from '../recoilModel'; +import { skillsStateSelector } from '../recoilModel'; import { DialogWrapper, DialogTypes } from './DialogWrapper'; @@ -31,7 +30,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: SkillSetting) => void; + onSubmit: (manifestUrl: string, endpointName: string) => void; onDismiss: () => void; } @@ -120,9 +119,12 @@ export const validateName = ({ }; export const CreateSkillModal: React.FC = ({ projectId, onSubmit, onDismiss }) => { - const skills = useRecoilValue(skillsState(projectId)); + const skills = useRecoilValue(skillsStateSelector); - const [formData, setFormData] = useState>({}); + const [formData, setFormData] = useState<{ manifestUrl: string; endpoint: string }>({ + manifestUrl: '', + endpoint: '', + }); const [formDataErrors, setFormDataErrors] = useState({}); const [validationState, setValidationState] = useState({ endpoint: ValidationState.NotValidated, @@ -175,18 +177,6 @@ export const CreateSkillModal: React.FC = ({ projectId, o setSelectedEndpointKey(null); }; - const handleNameChange = (_, name = '') => { - setValidationState((validationState) => ({ ...validationState, name: ValidationState.NotValidated })); - debouncedValidateName({ - formData: { ...formData, name }, - ...validationHelpers, - }); - setFormData({ - ...formData, - name, - }); - }; - const handleEndpointUrlChange = (_, option?: IDropdownOption) => { if (option) { const { data, key } = option; @@ -212,7 +202,7 @@ export const CreateSkillModal: React.FC = ({ projectId, o Object.values(validationState).every((validation) => validation === ValidationState.Validated) && !Object.values(formDataErrors).some(Boolean) ) { - onSubmit({ name: skillManifest.name, ...formData } as SkillSetting); + onSubmit(formData.manifestUrl, formData.endpointName); } }; @@ -252,12 +242,6 @@ export const CreateSkillModal: React.FC = ({ projectId, o }} /> )} - = (props) => { onDismiss={dismissCallout} onTry={handleStart} /> - {settings.luis && modalOpen && ( - - )} + {settings.luis && modalOpen && } ); }; diff --git a/Composer/packages/client/src/components/TestController/startBotsDialog.tsx b/Composer/packages/client/src/components/TestController/startBotsDialog.tsx new file mode 100644 index 0000000000..9dd61cb307 --- /dev/null +++ b/Composer/packages/client/src/components/TestController/startBotsDialog.tsx @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import React, { useEffect, useState } from 'react'; +import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; +import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { DefaultButton, ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; +import formatMessage from 'format-message'; +import { + DetailsList, + DetailsListLayoutMode, + IDetailsRowProps, + SelectionMode, +} from 'office-ui-fabric-react/lib/DetailsList'; +import { useRecoilValue } from 'recoil'; +import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; +import { SharedColors, FontSizes } from '@uifabric/fluent-theme'; + +import { botProjectSpaceSelector } from '../../recoilModel'; + +const styles = { + detailListContainer: css` + flex-grow: 1; + height: 350px; + position: relative; + overflow: hidden; + `, +}; + +export const actionButton = css` + font-size: 14px; + margin-top: 2px; + color: #0078d4; +`; + +const dialog = { + title: { + fontWeight: FontWeights.bold, + }, +}; + +const rowHeader = { display: 'flex', alignItems: 'center' }; +const statusStyle = { + color: SharedColors.green20, +}; + +const icon = () => + ({ + root: { + color: `${SharedColors.cyanBlue20}`, + marginRight: '12px', + boxSizing: 'border-box', + fontSize: `${FontSizes.size16}`, + width: '20px', + }, + } as IButtonStyles); + +// eslint-disable-next-line react/display-name + +interface IStartBotsDialogProps { + isOpen: boolean; + onDismiss: () => void; +} + +export const StartBotsDialog: React.FC = (props) => { + const { isOpen, onDismiss } = props; + const projectCollection = useRecoilValue(botProjectSpaceSelector); + + const [items, setItems] = useState<{ displayName: string; status: string }[]>([]); + const [allBotsStarted, setAllBotsStarted] = useState(false); + + useEffect(() => { + const transformedItems: any[] = projectCollection.map((projectData) => { + return { + displayName: projectData.name, + status: 'Running', + }; + }); + setItems(transformedItems); + }, [projectCollection]); + + const onRenderRow = (props?: IDetailsRowProps, defaultRender?: IRenderFunction): JSX.Element => { + return
{defaultRender && defaultRender(props)}
; + }; + + const tableColumns = [ + { + key: 'column1', + name: formatMessage('Bot'), + fieldName: 'id', + isRowHeader: true, + isResizable: true, + isSortedDescending: false, + sortAscendingAriaLabel: formatMessage('Sorted A to Z'), + sortDescendingAriaLabel: formatMessage('Sorted Z to A'), + data: 'string', + onRender: (item: any) => { + return ( +
+ + {item.displayName} +
+ ); + }, + isPadded: true, + }, + { + key: 'column2', + name: formatMessage('Status'), + fieldName: 'type', + isRowHeader: true, + isResizable: true, + isSorted: true, + data: 'string', + onRender: (item: any) => { + return ( + + {item.status} + + ); + }, + isPadded: true, + }, + ]; + + return ( + + ); +}; diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 3969cc7eb1..2826792d47 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -61,6 +61,7 @@ import { deleteDialogContent, editorContainer, editorWrapper, + manifestUrl, pageRoot, visualPanel, } from './styles'; @@ -139,7 +140,8 @@ const DesignPage: React.FC { + addRemoteSkillToBotProject( + 'https://onenote-dev.azurewebsites.net/manifests/OneNoteSync-2-1-preview-1-manifest.json', + 'remote' + ); + }, + }, + { + 'data-testid': 'AddLocalSkill', + key: 'addLocalSkill', + text: formatMessage(`Add local skill from path`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + addExistingSkillToBotProject('/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync'); + }, + }, { key: 'edit.undo', text: formatMessage('Undo'), @@ -670,7 +695,7 @@ const DesignPage: React.FC addSkillDialogCancel(projectId)} - onSubmit={(skill) => addSkill(projectId, skill)} + onSubmit={addRemoteSkillToBotProject} /> )} {exportSkillModalVisible && ( diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts b/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts index 767006135c..712441ac88 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts +++ b/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import get from 'lodash/get'; -import { DialogInfo, DialogSchemaFile, ITrigger, SDKKinds, SkillManifest, LuFile, QnAFile } from '@bfc/shared'; +import { DialogInfo, DialogSchemaFile, ITrigger, SDKKinds, SkillManifestFile, LuFile, QnAFile } from '@bfc/shared'; import { JSONSchema7 } from '@bfc/extension-client'; import { Activities, Activity, activityHandlerMap, ActivityTypes, DispatchModels } from './constants'; @@ -13,7 +13,7 @@ export const isSupportedTrigger = ({ type }: ITrigger) => Object.keys(activityHa export const generateSkillManifest = ( schema: JSONSchema7, - skillManifest: Partial, + skillManifest: Partial, dialogs: DialogInfo[], dialogSchemas: DialogSchemaFile[], luFiles: LuFile[], diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx index 43424dd313..6fb822fbfd 100644 --- a/Composer/packages/client/src/pages/skills/skill-list.tsx +++ b/Composer/packages/client/src/pages/skills/skill-list.tsx @@ -20,9 +20,10 @@ import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { FontSizes } from '@uifabric/fluent-theme'; import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; +import { Skill } from '@bfc/shared'; -import { DisplayManifestModal } from '../../components/Modal/DisplayManifestModal'; -import { dispatcherState, skillsState } from '../../recoilModel'; +// import { DisplayManifestModal } from '../../components/Modal/DisplayManifestModal'; +import { dispatcherState, skillsStateSelector } from '../../recoilModel'; import { TableView, TableCell } from './styles'; @@ -120,8 +121,8 @@ interface SkillListProps { } const SkillList: React.FC = ({ projectId }) => { - const { removeSkill, updateSkill } = useRecoilValue(dispatcherState); - const skills = useRecoilValue(skillsState(projectId)); + const { removeSkillFromBotProject, addRemoteSkillToBotProject } = useRecoilValue(dispatcherState); + const skills = useRecoilValue(skillsStateSelector); const [selectedSkillUrl, setSelectedSkillUrl] = useState(null); @@ -131,20 +132,26 @@ const SkillList: React.FC = ({ projectId }) => { } }; - const handleEditSkill = (projectId, skillId) => (skillData) => { - updateSkill(projectId, skillId, skillData); + const handleEditSkill = (projectId, skillId) => (skillData) => {}; + + const generateSkillsList = (skills: Record) => { + const skillsList: any[] = []; + for (const skillName in skills) { + const currentSkill = skills[skillName]; + skillsList.push({ + skill: { + name: currentSkill.name, + description: currentSkill.description, + onDelete: () => removeSkillFromBotProject(currentSkill.id), + onViewManifest: () => handleViewManifest(currentSkill.id), + onEditSkill: handleEditSkill(projectId, currentSkill.id), + }, + }); + } + return skillsList; }; - const items = useMemo( - () => - skills.map((skill) => ({ - skill, - onDelete: () => removeSkill(projectId, skill.id), - onViewManifest: () => handleViewManifest(skill), - onEditSkill: handleEditSkill(projectId, skill.id), - })), - [skills, projectId] - ); + const items = useMemo(() => generateSkillsList(skills), [skills, projectId]); const onDismissManifest = () => { setSelectedSkillUrl(null); @@ -179,13 +186,13 @@ const SkillList: React.FC = ({ projectId }) => { />
- + /> */} ); }; diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 4c50533561..cb8c757f30 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -10,10 +10,10 @@ import { LuFile, QnAFile, BotSchemas, - Skill, DialogSetting, BotProjectSpace, BotProjectFile, + SkillManifestFile, } from '@bfc/shared'; import { BotLoadError, DesignPageLocation, QnAAllUpViewStatus } from '../../recoilModel/types'; @@ -108,13 +108,6 @@ export const luFilesState = atomFamily({ }, }); -export const skillsState = atomFamily({ - key: getFullyQualifiedKey('skills'), - default: (id) => { - return []; - }, -}); - export const actionsSeedState = atomFamily({ key: getFullyQualifiedKey('actionsSeed'), default: (id) => { @@ -122,7 +115,7 @@ export const actionsSeedState = atomFamily({ }, }); -export const skillManifestsState = atomFamily({ +export const skillManifestsState = atomFamily({ key: getFullyQualifiedKey('skillManifests'), default: (id) => { return []; @@ -279,3 +272,9 @@ export const botNameIdentifierState = atomFamily({ key: getFullyQualifiedKey('botNameIdentifier'), default: '', }); + +// TODO: Currently always setting to 0 as we dont support more than 1 manifest. This index would need to change based on the default manifest chosen in the future. +export const currentSkillManifestIndexState = atomFamily({ + key: getFullyQualifiedKey('currentSkillManifestIndex'), + default: 0, +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index da7ac2075f..5012f4d2da 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -32,7 +32,6 @@ import { localeState, schemasState, locationState, - skillsState, botStatusState, botDisplayNameState, botOpeningState, 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 61ebcd3c70..1d47fdf395 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts @@ -10,7 +10,6 @@ import { renderRecoilHook } from '../../../../__tests__/testUtils'; import { skillManifestsState, onAddSkillDialogCompleteState, - skillsState, settingsState, showAddSkillDialogModalState, displaySkillManifestState, @@ -42,7 +41,7 @@ const makeTestSkill: (number) => Skill = (n) => ({ endpointUrl: 'url', endpoints: [{ test: 'foo' }], msAppId: 'ID', - content: { + manifestContent: { description: 'test skill' + n, endpoints: [{ test: 'foo' }], }, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index ad523b1498..ceb9c265cd 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -4,27 +4,49 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpaceSkill, convertAbsolutePathToFileProtocol } from '@bfc/shared'; +import { BotProjectSpaceSkill, convertAbsolutePathToFileProtocol, Skill } from '@bfc/shared'; -import { botNameIdentifierState, botProjectFileState, locationState } from '../atoms'; +import { botNameIdentifierState, botProjectFileState, locationState, settingsState } from '../atoms'; import { rootBotProjectIdSelector } from '../selectors'; +import { dispatcherState } from '../DispatcherWrapper'; export const botProjectFileDispatcher = () => { - const addLocalSkillToBotProjectFile = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const addLocalSkill = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + return; + } + const skillLocation = await snapshot.getPromise(locationState(skillId)); + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + const skill: BotProjectSpaceSkill = { + workspace: convertAbsolutePathToFileProtocol(skillLocation), + remote: false, + }; + draftState.content.skills[botName] = skill; + }); + return result; + }); + }); + + const addRemoteSkill = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillId: string, manifestUrl: string, endpointName: string) => { const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; } - const skillLocation = await snapshot.getPromise(locationState(skillId)); const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { const skill: BotProjectSpaceSkill = { - workspace: convertAbsolutePathToFileProtocol(skillLocation), - remote: false, + manifest: manifestUrl, + remote: true, + endpointName, }; + draftState.content.skills[botName] = skill; }); return result; @@ -32,40 +54,109 @@ export const botProjectFileDispatcher = () => { } ); - const addRemoteSkillToBotProjectFile = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillId: string, manifestUrl: string, endpointName: string) => { + const removeSkill = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + return; + } + + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + delete draftState.content.skills[botName]; + }); + return result; + }); + }); + + const updateManifest = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillProjectId: string, manifestId: string | undefined) => { const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; } - const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + const skillNameIdentifier = await snapshot.getPromise(botNameIdentifierState(skillProjectId)); set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { - const skill: BotProjectSpaceSkill = { - manifest: manifestUrl, - remote: true, - endpointName, - }; - - draftState.content.skills[botName] = skill; + if (!manifestId) { + delete draftState[skillNameIdentifier].manifest; + } else { + draftState[skillNameIdentifier] = { + ...draftState[skillNameIdentifier], + manifest: manifestId, + }; + } }); return result; }); } ); - const removeSkillFromBotProjectFile = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const updateSkillsData = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async ( + skillNameIdentifier: string, + skillsData: Skill, + selectedEndpointIndex: number + ) => { + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + return; + } + + const settings = await snapshot.getPromise(settingsState(rootBotProjectId)); + const dispatcher = await snapshot.getPromise(dispatcherState); + + let msAppId = '', + endpointUrl = '', + endpointName = ''; + + if (selectedEndpointIndex !== -1 && skillsData.manifest) { + const data = skillsData.manifest?.endpoints[selectedEndpointIndex]; + msAppId = data.msAppId; + endpointUrl = data.endpointUrl; + endpointName = data.name; + + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + draftState.content.skills[skillNameIdentifier].endpointName = endpointName; + }); + return result; + }); + } else { + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + delete draftState.content.skills[skillNameIdentifier].endpointName; + }); + return result; + }); + } + + dispatcher.setSettings( + rootBotProjectId, + produce(settings, (draftSettings) => { + draftSettings.skill = { + ...settings.skill, + [skillNameIdentifier]: { + endpointUrl, + msAppId, + }, + }; + }) + ); + } + ); + + const updateEndpointName = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillNameIdentifier: string, endpointName: string) => { const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; } - const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { - delete draftState.content.skills[botName]; + draftState.content.skills[skillNameIdentifier].endpointName = endpointName; }); return result; }); @@ -73,8 +164,11 @@ export const botProjectFileDispatcher = () => { ); return { - addLocalSkillToBotProjectFile, - removeSkillFromBotProjectFile, - addRemoteSkillToBotProjectFile, + addLocalSkillToBotProjectFile: addLocalSkill, + removeSkillFromBotProjectFile: removeSkill, + addRemoteSkillToBotProjectFile: addRemoteSkill, + updateSkillsDataInBotProjectFile: updateSkillsData, + updateManifestInBotProjectFile: updateManifest, + updateEndpointNameInBotProjectFile: updateEndpointName, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index c5673b3ba0..616e4b59f4 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -112,7 +112,7 @@ export const projectDispatcher = () => { ); const addRemoteSkillToBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { + (callbackHelpers: CallbackInterface) => async (manifestUrl: string, endpointName: string) => { const { set, snapshot } = callbackHelpers; try { const dispatcher = await snapshot.getPromise(dispatcherState); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts index df4b388635..7dae4460d9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts @@ -3,15 +3,12 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { CallbackInterface, useRecoilCallback } from 'recoil'; -import { SensitiveProperties, DialogSetting, PublishTarget, Skill } from '@bfc/shared'; -import { skillIndexer } from '@bfc/indexers'; +import { SensitiveProperties, DialogSetting, PublishTarget } from '@bfc/shared'; 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, skillsState } from '../atoms/botState'; +import { settingsState } from '../atoms/botState'; import httpClient from './../../utils/httpUtil'; import { setError } from './shared'; @@ -21,34 +18,7 @@ export const setSettingState = async ( 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/retrieveSkillManifest`, { - 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)); - } + const { set } = callbackHelpers; // set value in local storage for (const property of SensitiveProperties) { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts index c7938aacbd..70dba441a8 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts @@ -3,34 +3,56 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { CallbackInterface, useRecoilCallback } from 'recoil'; -import { SkillManifest, SkillSetting } from '@bfc/shared'; -import produce from 'immer'; +import { SkillManifestFile } from '@bfc/shared'; + +import { dispatcherState } from '../DispatcherWrapper'; import { skillManifestsState, onAddSkillDialogCompleteState, showAddSkillDialogModalState, displaySkillManifestState, - settingsState, } from './../atoms/botState'; -import { setSettingState } from './setting'; export const skillDispatcher = () => { - const createSkillManifest = ({ set }, { id, content, projectId }) => { - set(skillManifestsState(projectId), (skillManifests) => [...skillManifests, { content, id }]); + const createSkillManifest = async (callbackHelpers: CallbackInterface, { id, content, projectId }) => { + const { set, snapshot } = callbackHelpers; + let manifestForBotProjectFile = undefined; + const dispatcher = await snapshot.getPromise(dispatcherState); + set(skillManifestsState(projectId), (skillManifests) => { + if (!skillManifests.length) { + manifestForBotProjectFile = id; + } + return [...skillManifests, { content, id }]; + }); + if (manifestForBotProjectFile) { + dispatcher.updateManifestInBotProjectFile(projectId, id); + } }; const removeSkillManifest = useRecoilCallback( - ({ set }: CallbackInterface) => async (id: string, projectId: string) => { - set(skillManifestsState(projectId), (skillManifests) => skillManifests.filter((manifest) => manifest.id !== id)); + ({ set, snapshot }: CallbackInterface) => async (id: string, projectId: string) => { + let newCurrentManifestId: string | undefined = undefined; + const dispatcher = await snapshot.getPromise(dispatcherState); + set(skillManifestsState(projectId), (skillManifests) => { + const filtered = skillManifests.filter((manifest) => manifest.id !== id); + if (filtered.length > 0) { + newCurrentManifestId = filtered[0].id; + } + return filtered; + }); + dispatcher.updateManifestInBotProjectFile(projectId, newCurrentManifestId); } ); const updateSkillManifest = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ({ id, content }: SkillManifest, projectId: string) => { + ({ set, snapshot }: CallbackInterface) => async ({ id, content }: SkillManifestFile, projectId: string) => { const manifests = await snapshot.getPromise(skillManifestsState(projectId)); + const dispatcher = await snapshot.getPromise(dispatcherState); if (!manifests.some((manifest) => manifest.id === id)) { - createSkillManifest({ set }, { id, content, projectId }); + createSkillManifest({ set, snapshot }, { id, content, projectId }); + dispatcher.updateManifestInBotProjectFile(projectId, id); + return; } set(skillManifestsState(projectId), (skillManifests) => @@ -39,71 +61,6 @@ export const skillDispatcher = () => { } ); - const addSkill = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string, skill: SkillSetting) => { - const { set, snapshot } = callbackHelpers; - const { func: onAddSkillDialogComplete } = await snapshot.getPromise(onAddSkillDialogCompleteState(projectId)); - 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); - } - - set(showAddSkillDialogModalState(projectId), false); - set(onAddSkillDialogCompleteState(projectId), {}); - } - ); - - const removeSkill = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string, key: string) => { - const { snapshot } = callbackHelpers; - 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, - key: string, - { endpointUrl, manifestUrl, msAppId, name }: SkillSetting - ) => { - const { snapshot } = callbackHelpers; - const settings = await snapshot.getPromise(settingsState(projectId)); - - setSettingState( - callbackHelpers, - projectId, - produce(settings, (updateSettings) => { - updateSettings.skill = { - ...(updateSettings.skill || {}), - [key]: { - endpointUrl, - manifestUrl, - msAppId, - name, - }, - }; - }) - ); - } - ); - const addSkillDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete, projectId: string) => { set(showAddSkillDialogModalState(projectId), true); set(onAddSkillDialogCompleteState(projectId), { func: onComplete }); @@ -123,9 +80,6 @@ export const skillDispatcher = () => { }); return { - addSkill, - removeSkill, - updateSkill, createSkillManifest, removeSkillManifest, updateSkillManifest, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 4a1718dbe9..668f10a749 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -4,7 +4,6 @@ import { v4 as uuid } from 'uuid'; import { SensitiveProperties, - convertSkillsToDictionary, DialogSetting, dereferenceDefinitions, DialogInfo, @@ -14,6 +13,10 @@ import { BotProjectFile, BotProjectSpaceSkill, convertFileProtocolToPath, + getManifestNameFromUrl, + convertSkillsToDictionary, + SkillSetting, + getEndpointNameGivenUrl, } from '@bfc/shared'; import objectGet from 'lodash/get'; import objectSet from 'lodash/set'; @@ -57,7 +60,6 @@ import { settingsState, localeState, luFilesState, - skillsState, schemasState, lgFilesState, locationState, @@ -70,6 +72,7 @@ import { import { undoHistoryState } from '../../undo/history'; import { rootBotProjectIdSelector } from '../../selectors'; import { getUniqueName } from '../../../utils/fileUtil'; +import { dispatcherState } from '../../DispatcherWrapper'; export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); @@ -154,11 +157,11 @@ export const navigateToBot = ( }; const loadProjectData = (response) => { - const { files, botName, settings, skills: skillContent, id: projectId } = response.data; + const { files, botName, settings, id: projectId } = response.data; const mergedSettings = getMergedSettings(projectId, settings); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; - const indexedFiles = indexer.index(files, botName, locale, skillContent, mergedSettings); + const indexedFiles = indexer.index(files, botName, locale); return { botFiles: { ...indexedFiles, mergedSettings }, projectData: response.data, @@ -257,7 +260,7 @@ export const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialo export const initBotState = async (callbackHelpers: CallbackInterface, data: any, botFiles: any) => { const { snapshot, set } = callbackHelpers; const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; - const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, skills, mergedSettings } = botFiles; + const { dialogs, dialogSchemas, luFiles, lgFiles, qnaFiles, skillManifestFiles, mergedSettings } = botFiles; const curLocation = await snapshot.getPromise(locationState(projectId)); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; @@ -293,13 +296,12 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(botStatusState(projectId), BotStatus.unConnected); set(locationState(projectId), location); } - set(skillsState(projectId), skills); set(schemasState(projectId), schemas); set(localeState(projectId), locale); set(botDiagnosticsState(projectId), diagnostics); refreshLocalStorage(projectId, settings); - set(settingsState(projectId), mergedSettings); + set(filePersistenceState(projectId), new FilePersistence(projectId)); set(undoHistoryState(projectId), new UndoHistory(projectId)); return mainDialog; @@ -341,6 +343,13 @@ export const openRemoteSkill = async ( set(botNameIdentifierState(projectId), botNameIdentifier); set(botDisplayNameState(projectId), manifestResponse.data.name); set(locationState(projectId), manifestUrl); + set(skillManifestsState(projectId), [ + { + content: manifestResponse.data, + id: getManifestNameFromUrl(manifestUrl), + lastModified: new Date().toDateString(), + }, + ]); return { projectId, manifestResponse: manifestResponse.data }; }; @@ -418,25 +427,56 @@ const handleSkillLoadingFailure = (callbackHelpers, { ex, skillNameIdentifier }) return projectId; }; +const migrateSkillsForExistingBots = ( + botProjectFile: BotProjectSpace, + rootBotSkill: SkillSetting & { manifestUrl: string } +) => { + if (Object.keys(botProjectFile.skills).length === 0 && Object.keys(rootBotSkill).length > 0) { + for (const skillName in rootBotSkill) { + const currentSkill = rootBotSkill[skillName]; + const skillNameIdentifier = camelCase(skillName); + + botProjectFile.skills[skillNameIdentifier] = { + manifest: currentSkill?.manifestUrl || '', + remote: true, + }; + } + } + return botProjectFile; +}; + +const fetchEndpointNameForSkill = (rootBotSettings: DialogSetting, skillNameIdentifier: string, manifestData) => { + const endpointUrl = objectGet(rootBotSettings, `skill[${skillNameIdentifier}].endpointUrl`); + if (endpointUrl) { + const matchedEndpoint = getEndpointNameGivenUrl(manifestData, endpointUrl); + return matchedEndpoint; + } +}; + const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, storageId = 'default') => { const { projectData, botFiles } = data; - const { set } = callbackHelpers; + const { set, snapshot } = callbackHelpers; + const dispatcher = await snapshot.getPromise(dispatcherState); const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); const rootBotProjectId = projectData.id; const { name } = projectData; + const { mergedSettings } = botFiles; + set(botNameIdentifierState(rootBotProjectId), camelCase(name)); + set(botProjectIdsState, [rootBotProjectId]); if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; - set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); + const updatedFileContent = migrateSkillsForExistingBots(currentBotProjectFileIndexed.content, mergedSettings.skill); + currentBotProjectFileIndexed.content = updatedFileContent; const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; - const skills: { [skillId: string]: BotProjectSpaceSkill } = { - ...currentBotProjectFile.skills, - }; + set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); + set(settingsState(rootBotProjectId), mergedSettings); + + const skills: { [skillId: string]: BotProjectSpaceSkill } = currentBotProjectFile.skills; - // RootBot loads first + skills load async const totalProjectsCount = Object.keys(skills).length + 1; if (totalProjectsCount > 0) { for (const nameIdentifier in skills) { @@ -450,8 +490,12 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st } if (skillPromise) { skillPromise - .then(({ projectId }) => { + .then(({ projectId, manifestResponse }) => { addProjectToBotProjectSpace(set, projectId, totalProjectsCount); + const matchedEndpoint = fetchEndpointNameForSkill(mergedSettings, nameIdentifier, manifestResponse); + if (matchedEndpoint) { + dispatcher.updateEndpointNameInBotProjectFile(nameIdentifier, matchedEndpoint); + } }) .catch((ex) => { const projectId = handleSkillLoadingFailure(callbackHelpers, { @@ -467,7 +511,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st // Should never hit here as all projects should have a botproject file throw new Error(formatMessage('Bot project file does not exist.')); } - set(botProjectIdsState, [rootBotProjectId]); + set(currentProjectIdState, rootBotProjectId); return { mainDialog, diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts index e6159e804a..2472d162ae 100644 --- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts +++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts @@ -186,6 +186,7 @@ class FilePersistence { private getBotProjectFileChanges(current: BotProjectFile, previous: BotProjectFile) { if (!isEqual(current, previous)) { + debugger; return [ { id: `${current.id}${FileExtensions.BotProject}`, diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index 2dbb6d83cb..f823d56f75 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -5,3 +5,4 @@ export * from './project'; export * from './eject'; export * from './extensions'; export * from './validatedDialogs'; +export * from './skills'; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index 7600eb326d..8233521904 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -57,3 +57,14 @@ export const rootBotProjectIdSelector = selector({ } }, }); + +export const skillsProjectIdSelector = selector({ + key: 'skillsProjectIdSelector', + get: ({ get }) => { + const botIds = get(botProjectIdsState); + return botIds.filter((projectId: string) => { + const { isRootBot } = get(projectMetaDataState(projectId)); + return !isRootBot; + }); + }, +}); diff --git a/Composer/packages/client/src/recoilModel/selectors/skills.ts b/Composer/packages/client/src/recoilModel/selectors/skills.ts new file mode 100644 index 0000000000..8533ad9ca3 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/skills.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Skill, Manifest } from '@bfc/types'; +import { selector } from 'recoil'; + +import { + skillManifestsState, + currentSkillManifestIndexState, + botNameIdentifierState, + botDisplayNameState, + projectMetaDataState, +} from '../atoms'; + +import { skillsProjectIdSelector } from './project'; + +// Actions +export const skillsStateSelector = selector({ + key: 'skillsStateSelector', + get: ({ get }) => { + const skillsProjectIds = get(skillsProjectIdSelector); + const skills: Record = skillsProjectIds.reduce((result, skillId: string) => { + const manifests = get(skillManifestsState(skillId)); + if (!manifests.length) { + return []; + } + const currentSkillManifestIndex = get(currentSkillManifestIndexState(skillId)); + const skillNameIdentifier = get(botNameIdentifierState(skillId)); + const botName = get(botDisplayNameState(skillId)); + const manifest: Manifest = manifests[currentSkillManifestIndex].content; + const { isRemote } = get(projectMetaDataState(skillId)); + if (skillNameIdentifier) { + result[skillNameIdentifier] = { + id: skillId, + manifest, + name: botName, + remote: isRemote, + }; + } + console.log('RESULT', result); + return result; + }, {}); + return skills; + }, +}); diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index bac740a100..d4dbba2467 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, DialogSchemaFile } from '@bfc/shared'; +import { ShellApi, ShellData, Shell, DialogSchemaFile, Skill, ManifestEndpoint } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; @@ -18,7 +18,6 @@ import { validateDialogSelectorFamily, breadcrumbState, focusPathState, - skillsState, localeState, qnaFilesState, designPageLocationState, @@ -28,6 +27,7 @@ import { luFilesState, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; +import { skillsStateSelector } from '../recoilModel/selectors'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -45,7 +45,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); const breadcrumb = useRecoilValue(breadcrumbState(projectId)); const focusPath = useRecoilValue(focusPathState(projectId)); - const skills = useRecoilValue(skillsState(projectId)); + const skills = useRecoilValue(skillsStateSelector); const locale = useRecoilValue(localeState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const undoFunction = useRecoilValue(undoFunctionState(projectId)); @@ -73,7 +73,7 @@ export function useShell(source: EventSource, projectId: string): Shell { updateUserSettings, setMessage, displayManifestModal, - updateSkill, + updateSkillsDataInBotProjectFile: updateEndpointInBotProjectFile, } = useRecoilValue(dispatcherState); const lgApi = useLgApi(projectId); @@ -203,7 +203,9 @@ export function useShell(source: EventSource, projectId: string): Shell { updateDialogSchema: async (dialogSchema: DialogSchemaFile) => { updateDialogSchema(dialogSchema, projectId); }, - updateSkillSetting: (...params) => updateSkill(projectId, ...params), + updateSkill: async (skillId: string, skillsData) => { + updateEndpointInBotProjectFile(skillId, skillsData.skill, skillsData.selectedEndpointIndex); + }, }; const currentDialog = useMemo(() => dialogs.find((d) => d.id === dialogId), [dialogs, dialogId]); diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index bbf1b42848..e7d8959547 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DialogSetting, FileInfo, importResolverGenerator } from '@bfc/shared'; +import { 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 { botProjectSpaceIndexer } from './botProjectSpaceIndexer'; import { FileExtensions } from './utils/fileExtensions'; @@ -46,7 +45,7 @@ class Indexer { return importResolverGenerator(lgFiles, '.lg', locale); }; - public index(files: FileInfo[], botName: string, locale: string, skillContent: any, settings: DialogSetting) { + public index(files: FileInfo[], botName: string, locale: string) { const result = this.classifyFile(files); return { dialogs: dialogIndexer.index(result[FileExtensions.Dialog], botName), @@ -55,7 +54,6 @@ 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), botProjectSpaceFiles: botProjectSpaceIndexer.index(result[FileExtensions.BotProjectSpace]), }; } diff --git a/Composer/packages/lib/indexers/src/skillManifestIndexer.ts b/Composer/packages/lib/indexers/src/skillManifestIndexer.ts index fa0529db15..451e0da6bf 100644 --- a/Composer/packages/lib/indexers/src/skillManifestIndexer.ts +++ b/Composer/packages/lib/indexers/src/skillManifestIndexer.ts @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo, SkillManifestInfo } from '@bfc/shared'; +import { FileInfo, SkillManifestFile } from '@bfc/shared'; import { getBaseName } from './utils/help'; const index = (skillManifestFiles: FileInfo[]) => { - return skillManifestFiles.reduce((manifests: SkillManifestInfo[], { content, name, lastModified }) => { + return skillManifestFiles.reduce((manifests: SkillManifestFile[], { content, name, lastModified }) => { try { const jsonContent = JSON.parse(content); return [...manifests, { content: jsonContent, id: getBaseName(name, '.json'), lastModified }]; } catch (error) { return manifests; } - }, [] as SkillManifestInfo[]); + }, [] as SkillManifestFile[]); }; export const skillManifestIndexer = { diff --git a/Composer/packages/lib/shared/src/skillsUtils/index.ts b/Composer/packages/lib/shared/src/skillsUtils/index.ts index c905f7a8c0..b3ba33beff 100644 --- a/Composer/packages/lib/shared/src/skillsUtils/index.ts +++ b/Composer/packages/lib/shared/src/skillsUtils/index.ts @@ -3,7 +3,14 @@ import get from 'lodash/get'; import keyBy from 'lodash/keyBy'; -import { DialogSetting, Skill } from '@bfc/types'; +import { BotProjectSpaceSkill, DialogSetting } from '@bfc/types'; +import formatMessage from 'format-message'; +import camelCase from 'lodash/camelCase'; + +export const VIRTUAL_LOCAL_ENDPOINT = { + key: -1, + name: formatMessage('Local Endpoint'), +}; export function fetchFromSettings(path: string, settings: DialogSetting): string { if (path) { @@ -13,8 +20,8 @@ export function fetchFromSettings(path: string, settings: DialogSetting): string return ''; } -export const convertSkillsToDictionary = (skills: Skill[]) => { - const mappedSkills = skills.map(({ msAppId, endpointUrl, manifestUrl, name }: Skill) => { +export const convertSkillsToDictionary = (skills: any[]) => { + const mappedSkills = skills.map(({ msAppId, endpointUrl, manifestUrl, name }) => { return { name, msAppId, @@ -23,7 +30,7 @@ export const convertSkillsToDictionary = (skills: Skill[]) => { }; }); - return keyBy(mappedSkills, 'name'); + return keyBy(mappedSkills, (item) => camelCase(item.name)); }; export const getSkillNameFromSetting = (value?: string) => { @@ -40,3 +47,11 @@ export const getEndpointNameGivenUrl = (manifestData: any, urlToMatch: string) = const matchedEndpoint = manifestData?.endpoints.find(({ endpointUrl }) => endpointUrl === urlToMatch); return matchedEndpoint ? matchedEndpoint.name : ''; }; + +export const getManifestNameFromUrl = (manifestUrl: string) => { + const manifestNameIndex = manifestUrl.lastIndexOf('/') + 1; + if (!manifestNameIndex) { + return manifestUrl; + } + return manifestUrl.substring(manifestNameIndex); +}; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 1e79db6aea..0f54e29044 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -9,7 +9,6 @@ import { autofixReferInDialog } from '@bfc/indexers'; import { getNewDesigner, FileInfo, - Skill, Diagnostic, IBotProject, DialogSetting, @@ -33,7 +32,6 @@ import AssetService from '../../services/asset'; import { Builder } from './builder'; import { IFileStorage } from './../storage/interface'; import { LocationRef, IBuildConfig } from './interface'; -import { retrieveSkillManifests } from './skillManager'; import { defaultFilePath, serializeFiles, parseFileName } from './botStructure'; const debug = log.extend('bot-project'); @@ -61,7 +59,6 @@ export class BotProject implements IBotProject { public defaultUISchema: { [key: string]: string; }; - public skills: Skill[] = []; public diagnostics: Diagnostic[] = []; public settingManager: ISettingManager; public settings: DialogSetting | null = null; @@ -160,9 +157,6 @@ export class BotProject implements IBotProject { public init = async () => { this.diagnostics = []; this.settings = await this.getEnvSettings(false); - const { skillManifests, diagnostics } = await retrieveSkillManifests(this.settings?.skill); - this.skills = skillManifests; - this.diagnostics.push(...diagnostics); this.files = await this._getFiles(); }; @@ -172,7 +166,6 @@ export class BotProject implements IBotProject { files: Array.from(this.files.values()), location: this.dir, schemas: this.getSchemas(), - skills: this.skills, diagnostics: this.diagnostics, settings: this.settings, }; diff --git a/Composer/packages/server/src/models/bot/skillManager.ts b/Composer/packages/server/src/models/bot/skillManager.ts index 88a5576af4..e09d08a847 100644 --- a/Composer/packages/server/src/models/bot/skillManager.ts +++ b/Composer/packages/server/src/models/bot/skillManager.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. import * as msRest from '@azure/ms-rest-js'; -import { SkillSetting, Diagnostic, DiagnosticSeverity } from '@bfc/shared'; -import toPairs from 'lodash/toPairs'; import logger from './../../logger'; @@ -25,28 +23,3 @@ export const getSkillManifest = async (url: string): Promise => { return typeof content === 'string' ? JSON.parse(content) : {}; }; - -export const retrieveSkillManifests = async (skillSettings?: { [name: string]: SkillSetting } | SkillSetting[]) => { - const skills = toPairs(skillSettings); - - const diagnostics: Diagnostic[] = []; - const skillManifests: any = []; - - for (const [id, { manifestUrl }] of skills) { - try { - const content = await getSkillManifest(manifestUrl); - - skillManifests.push({ content, id, manifestUrl }); - } catch (error) { - const notify = new Diagnostic( - `Accessing skill manifest url error, ${manifestUrl}`, - 'appsettings.json', - DiagnosticSeverity.Warning - ); - diagnostics.push(notify); - skillManifests.push({ id, manifestUrl }); - } - } - - return { diagnostics, skillManifests }; -}; diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index f2cf6f1356..d60f34c8ba 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -142,15 +142,26 @@ export type LgFile = { parseResult?: any; }; +export type Manifest = { + name: string; + version: string; + description: string; + endpoints: ManifestEndpoint[]; +}; + +export type ManifestEndpoint = { + name: string; + endpointUrl: string; + msAppId: string; + description: string; +}; + export type Skill = { id: string; - content: any; + manifest?: Manifest; description?: string; - endpoints: any[]; - endpointUrl: string; - manifestUrl: string; - msAppId: string; name: string; + remote: boolean; }; export type TextFile = { @@ -161,13 +172,7 @@ export type FileResolver = (id: string) => FileInfo | undefined; export type MemoryResolver = (id: string) => string[] | undefined; -export type SkillManifestInfo = { - content: { [key: string]: any }; - lastModified: string; - id: string; -}; - -export type SkillManifest = { +export type SkillManifestFile = { content: any; id: string; path?: string; @@ -180,7 +185,7 @@ export type BotAssets = { luFiles: LuFile[]; lgFiles: LgFile[]; qnaFiles: QnAFile[]; - skillManifests: SkillManifest[]; + skillManifests: SkillManifestFile[]; setting: DialogSetting; dialogSchemas: DialogSchemaFile[]; botProjectFile: BotProjectFile; diff --git a/Composer/packages/types/src/server.ts b/Composer/packages/types/src/server.ts index 3dd18f023b..600a5bd106 100644 --- a/Composer/packages/types/src/server.ts +++ b/Composer/packages/types/src/server.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Skill, FileInfo } from './indexers'; +import { FileInfo } from './indexers'; import { IDiagnostic } from './diagnostic'; import { DialogSetting } from './settings'; @@ -29,7 +29,6 @@ export type IBotProject = { defaultUISchema: { [key: string]: string; }; - skills: Skill[]; diagnostics: IDiagnostic[]; settingManager: ISettingManager; settings: DialogSetting | null; @@ -38,7 +37,6 @@ export type IBotProject = { files: FileInfo[]; location: string; schemas: any; - skills: Skill[]; diagnostics: IDiagnostic[]; settings: DialogSetting | null; }; diff --git a/Composer/packages/types/src/settings.ts b/Composer/packages/types/src/settings.ts index c5d987866f..eae924b1f4 100644 --- a/Composer/packages/types/src/settings.ts +++ b/Composer/packages/types/src/settings.ts @@ -23,8 +23,6 @@ export type AppUpdaterSettings = { }; export type SkillSetting = { - name: string; - manifestUrl: string; msAppId: string; endpointUrl: string; }; diff --git a/Composer/packages/types/src/shell.ts b/Composer/packages/types/src/shell.ts index d7e86aa874..442f4d0515 100644 --- a/Composer/packages/types/src/shell.ts +++ b/Composer/packages/types/src/shell.ts @@ -6,6 +6,8 @@ import type { DialogInfo, LuFile, LgFile, QnAFile, LuIntentSection, LgTemplate, import type { SkillSetting, UserSettings } from './settings'; import type { JSONSchema7 } from './schema'; +import { ManifestEndpoint, Skill } from '.'; + /** Recursively marks all properties as optional. */ type AllPartial = { [P in keyof T]?: T[P] extends (infer U)[] ? AllPartial[] : T[P] extends object ? AllPartial : T[P]; @@ -57,7 +59,7 @@ export type ShellData = { luFiles: LuFile[]; qnaFiles: QnAFile[]; userSettings: UserSettings; - skills: any[]; + skills: Record; skillsSettings: Record; // TODO: remove schemas: BotSchemas; @@ -101,7 +103,7 @@ export type ShellApi = { displayManifestModal: (manifestId: string) => void; updateDialogSchema: (_: DialogSchemaFile) => Promise; createTrigger: (id: string, formData, url?: string) => void; - updateSkillSetting: (skillId: string, skillsData: SkillSetting) => Promise; + updateSkill: (skillId: string, skillsData: { skill: Skill; selectedEndpointIndex: number }) => Promise; }; export type Shell = { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx index b553df3325..4e86d983a8 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx @@ -23,19 +23,25 @@ export const settingReferences = (skillName: string) => ({ export const SelectSkillDialogField: React.FC = (props) => { const { value, onChange } = props; - const { shellApi, skills = [] } = useShellApi(); - const { addSkillDialog, displayManifestModal } = shellApi; - const [comboboxTitle, setComboboxTitle] = useState(null); + + const { shellApi, skills } = useShellApi(); + const { displayManifestModal } = shellApi; + const [comboboxTitle, _] = useState(null); const skillId = getSkillNameFromSetting(value?.skillEndpoint); - const { content, manifestUrl, name } = skills.find(({ id }) => id === skillId) || ({} as Skill); + const { manifest, name }: Skill = skills[skillId] || {}; - const options: IComboBoxOption[] = skills.map(({ id, name }) => ({ - key: name, - text: name, - data: settingReferences(id), - isSelected: id === skillId, - })); + const options: IComboBoxOption[] = []; + for (const skillNameIdentifier in skills) { + const skill = skills[skillNameIdentifier]; + const option = { + key: skillNameIdentifier, + text: skill.name, + data: settingReferences(skillNameIdentifier), + isSelected: skillNameIdentifier === skillId, + }; + options.push(option); + } options.push( { @@ -52,17 +58,7 @@ export const SelectSkillDialogField: React.FC = (props) => { const handleChange = (_, option: IComboBoxOption) => { if (option) { - if (option.key === ADD_DIALOG) { - setComboboxTitle(formatMessage('Add a new Skill Dialog')); - addSkillDialog().then((skill) => { - if (skill?.manifestUrl && skill?.name) { - onChange({ ...value, ...settingReferences(skill.name) }); - } - setComboboxTitle(null); - }); - } else { - onChange({ ...value, ...option.data }); - } + onChange({ ...value, ...option.data }); } }; @@ -78,9 +74,9 @@ export const SelectSkillDialogField: React.FC = (props) => { onChange={handleChange} /> manifestUrl && displayManifestModal(manifestUrl)} + onClick={() => manifest && displayManifestModal(name)} > {formatMessage('Show skill manifest')} diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx index 6f3cc2d6a0..6698d62af7 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx @@ -5,37 +5,62 @@ import React, { useMemo } from 'react'; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { FieldProps, useShellApi } from '@bfc/extension-client'; import { FieldLabel } from '@bfc/adaptive-form'; -import { getSkillNameFromSetting, Skill } from '@bfc/shared'; +import { getSkillNameFromSetting, Skill, VIRTUAL_LOCAL_ENDPOINT } from '@bfc/shared'; +import { SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/ComboBox'; export const SkillEndpointField: React.FC = (props) => { const { description, label, required, uiOptions, value } = props; - const { shellApi, skillsSettings, skills = [] } = useShellApi(); - const { updateSkillSetting } = shellApi; + const { shellApi, skillsSettings, skills } = useShellApi(); + const { updateSkill } = shellApi; const id = getSkillNameFromSetting(value?.skillEndpoint); - const skill = skills.find(({ id: skillId }) => skillId === id) || ({} as Skill); - const { endpointUrl, msAppId } = skillsSettings[id] || {}; - - const { endpoints = [] } = skill; - - const options = useMemo( - () => - endpoints.map(({ name, endpointUrl, msAppId }, key) => ({ - key, - text: name, - data: { - endpointUrl, - msAppId, + const skill: Skill = skills[id] || {}; + + const { endpointUrl: endpointUrlInSettings, msAppId: msAppIdInSettings } = skillsSettings[id] || {}; + + const endpoints = skill?.manifest?.endpoints || []; + + const options = useMemo(() => { + const endpointsInManifest = endpoints.map(({ name, endpointUrl, msAppId }, key) => ({ + key, + text: name, + data: { + endpointUrl, + msAppId, + name, + }, + isManifestEndpoint: true, + })); + + let localEndpoint: any[] = []; + if (!skill.remote) { + localEndpoint = [ + { + key: -1, + text: VIRTUAL_LOCAL_ENDPOINT.name, + data: { + endpointUrl: endpointUrlInSettings, + msAppId: msAppIdInSettings, + name: VIRTUAL_LOCAL_ENDPOINT.name, + }, }, - })), - [endpoints] - ); + { + key: 'separator', + itemType: SelectableOptionMenuItemType.Divider, + text: '', + }, + ]; + } + return [...localEndpoint, ...endpointsInManifest]; + }, [endpoints]); - const { key } = options.find(({ data }) => data.endpointUrl === endpointUrl && data.msAppId === msAppId) || {}; + const { key } = options.find(({ data, isManifestEndpoint }) => { + return isManifestEndpoint && data?.endpointUrl === endpointUrlInSettings && data?.msAppId === msAppIdInSettings; + }) || { key: -1 }; const handleChange = (_: React.FormEvent, option?: IDropdownOption) => { if (option) { - updateSkillSetting(skill.id, { ...skill, ...option.data }); + updateSkill(id, { skill: { ...skill }, selectedEndpointIndex: Number(option.key) }); } }; 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 1d6cc2c7ce..e32c67e144 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 @@ -4,6 +4,7 @@ import React from 'react'; import { act, fireEvent, getAllByRole, render } from '@bfc/test-utils'; import { EditorExtension } from '@bfc/extension-client'; +import { ShellData } from '@bfc/shared'; import { SelectSkillDialogField, settingReferences } from '../SelectSkillDialogField'; @@ -15,13 +16,13 @@ const renderSelectSkillDialog = ({ addSkillDialog = jest.fn(), onChange = jest.f onChange, } as any; - const shell = { + const shell: any = { addSkillDialog, }; - const shellData = { + const shellData: ShellData = { skills, - }; + } as ShellData; return render( diff --git a/Composer/plugins/azurePublish/yarn.lock b/Composer/plugins/azurePublish/yarn.lock index 90ad7db056..683e4038e2 100644 --- a/Composer/plugins/azurePublish/yarn.lock +++ b/Composer/plugins/azurePublish/yarn.lock @@ -160,9 +160,12 @@ version "1.0.0" dependencies: debug "^4.1.1" + fs-extra "^9.0.1" globby "^11.0.0" + node-fetch "^2.6.1" passport "^0.4.1" path-to-regexp "^6.1.0" + tar "^6.0.5" "@bfc/indexers@../../packages/lib/indexers": version "0.0.0" @@ -912,6 +915,11 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + clean-stack@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" @@ -1418,6 +1426,13 @@ fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^1.0.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1956,6 +1971,26 @@ minimist@1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + moment-timezone@*, moment-timezone@^0.5.28: version "0.5.31" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" @@ -1998,7 +2033,7 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.6.0, node-fetch@~2.6.0: +node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@~2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -2449,6 +2484,18 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" + integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2631,6 +2678,11 @@ yallist@^3.0.2: resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha1-27fa+b/YusmrRev2ArjLrQ1dCP0= +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + zip-stream@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.2.tgz#3a20f1bd7729c2b59fd4efa04df5eb7a5a217d2e" From c1723b45f3f03dc90334a9344478b860006cdd3e Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 12 Oct 2020 14:59:57 -0700 Subject: [PATCH 033/100] Runtime start updates Signed-off-by: Srinaath Ravichandran Bot load status Signed-off-by: Srinaath Ravichandran Bot status indicator Signed-off-by: Srinaath Ravichandran Notification selector Signed-off-by: Srinaath Ravichandran --- .../packages/client/src/components/Header.tsx | 15 +- .../src/components/NotificationContainer.tsx | 2 +- .../TestController/BotRuntimeOperation.tsx | 68 +++++++++ .../TestController/BotStatusIndicator.tsx | 94 ++++++++++++ .../TestController/BotStatusText.tsx | 138 ++++++++++++++++++ .../TestController/TestController.tsx | 22 ++- .../TestController/startBotsDialog.tsx | 70 ++++----- .../client/src/recoilModel/atoms/botState.ts | 2 +- .../src/recoilModel/dispatchers/builder.ts | 6 +- .../src/recoilModel/dispatchers/project.ts | 4 +- .../src/recoilModel/dispatchers/publisher.ts | 26 +++- .../recoilModel/dispatchers/utils/project.ts | 2 +- .../selectors/botRuntimeOperations.ts | 75 ++++++++++ ...ificationsSelector.ts => notifications.ts} | 0 .../src/recoilModel/selectors/project.ts | 20 ++- .../lib/shared/src/skillsUtils/index.ts | 2 +- Composer/packages/types/src/shell.ts | 2 +- .../select-skill-dialog/src/ComboBoxField.tsx | 2 +- .../src/SelectSkillDialogField.tsx | 21 +-- 19 files changed, 486 insertions(+), 85 deletions(-) create mode 100644 Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx create mode 100644 Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx create mode 100644 Composer/packages/client/src/components/TestController/BotStatusText.tsx create mode 100644 Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts rename Composer/packages/client/src/recoilModel/selectors/{notificationsSelector.ts => notifications.ts} (100%) diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index c4bc3796e9..d17692e553 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -5,7 +5,7 @@ import { jsx, css } from '@emotion/core'; import formatMessage from 'format-message'; import { IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; -import { useCallback, Fragment } from 'react'; +import { useCallback, Fragment, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { SharedColors } from '@uifabric/fluent-theme'; import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; @@ -20,6 +20,8 @@ import { import composerIcon from '../images/composerIcon.svg'; import { AppUpdaterStatus } from '../constants'; +import { StartBotsDialog } from './TestController/startBotsDialog'; + // -------------------- Styles -------------------- // const headerContainer = css` @@ -85,11 +87,20 @@ export const Header = () => { const locale = useRecoilValue(localeState(projectId)); const appUpdate = useRecoilValue(appUpdateState); const { showing, status } = appUpdate; + const [showStartBotsPanel, setStartPanelView] = useState(false); const onUpdateAvailableClick = useCallback(() => { setAppUpdateShowing(true); }, []); + function dismissStartPanelViewer() { + setStartPanelView(false); + } + + function showStartPanelViewer() { + setStartPanelView(true); + } + const showUpdateAvailableIcon = status === AppUpdaterStatus.UPDATE_AVAILABLE && !showing; return ( @@ -117,6 +128,8 @@ export const Header = () => { onClick={onUpdateAvailableClick} /> )} + + {showStartBotsPanel && }
); }; diff --git a/Composer/packages/client/src/components/NotificationContainer.tsx b/Composer/packages/client/src/components/NotificationContainer.tsx index afdc87b5a2..36f026361e 100644 --- a/Composer/packages/client/src/components/NotificationContainer.tsx +++ b/Composer/packages/client/src/components/NotificationContainer.tsx @@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil'; import React from 'react'; import { dispatcherState } from '../recoilModel'; -import { notificationsSelector } from '../recoilModel/selectors/notificationsSelector'; +import { notificationsSelector } from '../recoilModel/selectors/notifications'; import { NotificationCard } from './NotificationCard'; diff --git a/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx b/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx new file mode 100644 index 0000000000..4b830aea75 --- /dev/null +++ b/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { Fragment } from 'react'; +import { useRecoilValue } from 'recoil'; +import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; +import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { IPublishConfig } from '@bfc/shared'; + +import { botStatusState } from '../../recoilModel'; +import { BotStatus } from '../../constants'; +import { + botBuildRequiredSelector, + botRuntimeOperationsSelector, + isBuildConfigCompleteSelector, +} from '../../recoilModel/selectors/botRuntimeOperations'; + +interface BotStartOperationProps { + projectId: string; + displayName: string; +} + +const icon: IButtonStyles = { + root: { + color: `${SharedColors.cyanBlue20}`, + marginRight: '12px', + boxSizing: 'border-box', + fontSize: `${FontSizes.size16}`, + width: '20px', + }, +}; + +export const BotRuntimeOperation: React.FC = ({ projectId, displayName }) => { + const currentBotStatus = useRecoilValue(botStatusState(projectId)); + const botBuildRequired = useRecoilValue(botBuildRequiredSelector(projectId)); + const { config: startBotConfig } = useRecoilValue(isBuildConfigCompleteSelector(projectId)); + const operations = useRecoilValue(botRuntimeOperationsSelector); + + const handleBotStop = () => { + operations?.onStop(projectId); + }; + + const handleBotStart = async () => { + let config: IPublishConfig | undefined = undefined; + if (botBuildRequired) { + config = startBotConfig; + } + operations?.onStart(projectId, config); + }; + + return ( + + {currentBotStatus === BotStatus.published ? ( + + ) : ( + + )} + {displayName} + + ); +}; diff --git a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx new file mode 100644 index 0000000000..9eff0d36e1 --- /dev/null +++ b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { Fragment, useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { defaultPublishConfig } from '@bfc/shared'; +import { SharedColors } from '@uifabric/fluent-theme'; + +import { BotStatus } from '../../constants'; +import { botEndpointsState, botStatusState, dispatcherState } from '../../recoilModel'; + +import { EmulatorOpenButton } from './emulatorOpenButton'; + +const POLLING_INTERVAL = 2500; +let botStatusInterval: NodeJS.Timeout | undefined = undefined; + +interface BotStatusIndicatorProps { + projectId: string; +} + +const statusStyle = { + color: SharedColors.green20, +}; + +export const BotStatusIndicator: React.FC = ({ projectId }) => { + const currentBotStatus = useRecoilValue(botStatusState(projectId)); + const botEndpoints = useRecoilValue(botEndpointsState); + const { openBotInEmulator, setBotStatus, getPublishStatus } = useRecoilValue(dispatcherState); + const [botStatusText, setBotStatusText] = useState(''); + + // TODO: Build a useInterval hook instead of doing it in component. + function stopPollingRuntime() { + if (botStatusInterval) { + clearInterval(botStatusInterval); + botStatusInterval = undefined; + } + } + + function startPollingRuntime() { + if (!botStatusInterval) { + const cancelInterval = setInterval(() => { + // get publish status + getPublishStatus(projectId, defaultPublishConfig); + }, POLLING_INTERVAL); + botStatusInterval = cancelInterval; + } + } + + useEffect(() => { + switch (currentBotStatus) { + case BotStatus.failed: + stopPollingRuntime(); + setBotStatus(projectId, BotStatus.pending); + break; + case BotStatus.published: + stopPollingRuntime(); + break; + case BotStatus.reloading: + startPollingRuntime(); + break; + default: + case BotStatus.connected: + stopPollingRuntime(); + break; + } + + if (currentBotStatus === BotStatus.connected) { + setBotStatusText('Started'); + } else if (currentBotStatus === BotStatus.failed) { + setBotStatusText('Error occured'); + } else { + setBotStatusText('Inactive'); + } + + return () => { + stopPollingRuntime(); + return; + }; + }, [currentBotStatus]); + + return ( + + {botStatusText} + + ); +}; diff --git a/Composer/packages/client/src/components/TestController/BotStatusText.tsx b/Composer/packages/client/src/components/TestController/BotStatusText.tsx new file mode 100644 index 0000000000..7a7a1dbfd7 --- /dev/null +++ b/Composer/packages/client/src/components/TestController/BotStatusText.tsx @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import React, { useEffect, useState } from 'react'; +import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; +import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { DefaultButton, ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; +import formatMessage from 'format-message'; +import { + DetailsList, + DetailsListLayoutMode, + IDetailsRowProps, + SelectionMode, +} from 'office-ui-fabric-react/lib/DetailsList'; +import { useRecoilValue } from 'recoil'; +import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; +import { SharedColors, FontSizes } from '@uifabric/fluent-theme'; + +import { botProjectSpaceSelector } from '../../recoilModel'; + +interface IStartBotsDialogProps { + isOpen: boolean; + onDismiss: () => void; +} + +export const StartBotsDialog: React.FC = (props) => { + const { isOpen, onDismiss } = props; + const projectCollection = useRecoilValue(botProjectSpaceSelector); + + const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); + const [allBotsStarted, setAllBotsStarted] = useState(false); + + useEffect(() => { + const result = projectCollection.map(({ name, projectId }) => ({ displayName: name, projectId })); + setItems(result); + }, [projectCollection]); + + const onRenderRow = (props?: IDetailsRowProps, defaultRender?: IRenderFunction): JSX.Element => { + return
{defaultRender && defaultRender(props)}
; + }; + + const startAllBots = () => { + setAllBotsStarted(true); + }; + + const stopAllBots = () => { + setAllBotsStarted(false); + }; + + const tableColumns = [ + { + key: 'column1', + name: formatMessage('Bot'), + fieldName: 'id', + isRowHeader: true, + isResizable: true, + isSortedDescending: false, + sortAscendingAriaLabel: formatMessage('Sorted A to Z'), + sortDescendingAriaLabel: formatMessage('Sorted Z to A'), + data: 'string', + onRender: (item: any) => { + return ( +
+ + {item.displayName} +
+ ); + }, + isPadded: true, + }, + { + key: 'column2', + name: formatMessage('Status'), + fieldName: 'type', + isRowHeader: true, + isResizable: true, + isSorted: true, + data: 'string', + onRender: (item: any) => { + return ( + + {item.status} + + ); + }, + isPadded: true, + }, + ]; + + return ( + + ); +}; diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index c415d7f463..4c4d9969ea 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -19,7 +19,7 @@ import { luFilesState, qnaFilesState, settingsState, - botLoadErrorState, + botRuntimeErrorState, } from '../../recoilModel'; import settingsStorage from '../../utils/dialogSettingStorage'; import { BotStatus } from '../../constants'; @@ -27,7 +27,6 @@ import { isAbsHosted } from '../../utils/envUtil'; import useNotifications from '../../pages/notifications/useNotifications'; import { navigateTo, openInEmulator } from '../../utils/navigation'; -import { StartBotsDialog } from './startBotsDialog'; import { isBuildConfigComplete, needsBuild } from './../../utils/buildUtil'; import { PublishDialog } from './publishDialog'; import { ErrorCallout } from './errorCallout'; @@ -67,7 +66,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { const luFiles = useRecoilValue(luFilesState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); - const botLoadErrorMsg = useRecoilValue(botLoadErrorState(projectId)); + const botLoadErrorMsg = useRecoilValue(botRuntimeErrorState(projectId)); const botEndpoints = useRecoilValue(botEndpointsState); const { @@ -100,7 +99,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { case BotStatus.failed: openCallout(); stopPollingRuntime(); - setBotStatus(BotStatus.pending, projectId); + setBotStatus(projectId, BotStatus.pending); break; case BotStatus.published: stopPollingRuntime(); @@ -154,7 +153,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { } async function handleBuild(config: IPublishConfig) { - setBotStatus(BotStatus.publishing, projectId); + setBotStatus(projectId, BotStatus.publishing); dismissDialog(); const { luis, qna } = config; await setSettings(projectId, { @@ -166,7 +165,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { } async function handleLoadBot() { - setBotStatus(BotStatus.reloading, projectId); + setBotStatus(projectId, BotStatus.reloading); if (settings.qna && settings.qna.subscriptionKey) { await setQnASettings(projectId, settings.qna.subscriptionKey); } @@ -246,7 +245,16 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { onDismiss={dismissCallout} onTry={handleStart} /> - {settings.luis && modalOpen && } + {settings.luis && modalOpen && ( + + )} ); }; diff --git a/Composer/packages/client/src/components/TestController/startBotsDialog.tsx b/Composer/packages/client/src/components/TestController/startBotsDialog.tsx index 9dd61cb307..a4f563f218 100644 --- a/Composer/packages/client/src/components/TestController/startBotsDialog.tsx +++ b/Composer/packages/client/src/components/TestController/startBotsDialog.tsx @@ -8,7 +8,7 @@ import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; -import { DefaultButton, ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { DefaultButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; import formatMessage from 'format-message'; import { @@ -19,10 +19,12 @@ import { } from 'office-ui-fabric-react/lib/DetailsList'; import { useRecoilValue } from 'recoil'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; -import { SharedColors, FontSizes } from '@uifabric/fluent-theme'; import { botProjectSpaceSelector } from '../../recoilModel'; +import { BotStatusIndicator } from './BotStatusIndicator'; +import { BotRuntimeOperation } from './BotRuntimeOperation'; + const styles = { detailListContainer: css` flex-grow: 1; @@ -45,20 +47,6 @@ const dialog = { }; const rowHeader = { display: 'flex', alignItems: 'center' }; -const statusStyle = { - color: SharedColors.green20, -}; - -const icon = () => - ({ - root: { - color: `${SharedColors.cyanBlue20}`, - marginRight: '12px', - boxSizing: 'border-box', - fontSize: `${FontSizes.size16}`, - width: '20px', - }, - } as IButtonStyles); // eslint-disable-next-line react/display-name @@ -70,27 +58,29 @@ interface IStartBotsDialogProps { export const StartBotsDialog: React.FC = (props) => { const { isOpen, onDismiss } = props; const projectCollection = useRecoilValue(botProjectSpaceSelector); - - const [items, setItems] = useState<{ displayName: string; status: string }[]>([]); + const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); const [allBotsStarted, setAllBotsStarted] = useState(false); useEffect(() => { - const transformedItems: any[] = projectCollection.map((projectData) => { - return { - displayName: projectData.name, - status: 'Running', - }; - }); - setItems(transformedItems); + const result = projectCollection.map(({ name, projectId }) => ({ displayName: name, projectId })); + setItems(result); }, [projectCollection]); const onRenderRow = (props?: IDetailsRowProps, defaultRender?: IRenderFunction): JSX.Element => { return
{defaultRender && defaultRender(props)}
; }; - const tableColumns = [ + const startAllBots = () => { + setAllBotsStarted(true); + }; + + const stopAllBots = () => { + setAllBotsStarted(false); + }; + + const tableColumns: any = [ { - key: 'column1', + key: 'botName', name: formatMessage('Bot'), fieldName: 'id', isRowHeader: true, @@ -99,30 +89,21 @@ export const StartBotsDialog: React.FC = (props) => { sortAscendingAriaLabel: formatMessage('Sorted A to Z'), sortDescendingAriaLabel: formatMessage('Sorted Z to A'), data: 'string', - onRender: (item: any) => { - return ( -
- - {item.displayName} -
- ); + onRender: (item: { displayName: string; projectId: string }) => { + return ; }, isPadded: true, }, { - key: 'column2', + key: 'status', name: formatMessage('Status'), fieldName: 'type', isRowHeader: true, isResizable: true, isSorted: true, data: 'string', - onRender: (item: any) => { - return ( - - {item.status} - - ); + onRender: (item: { displayName: string; projectId: string }) => { + return ; }, isPadded: true, }, @@ -147,8 +128,11 @@ export const StartBotsDialog: React.FC = (props) => {
- {allBotsStarted ? : null} - {allBotsStarted ? Stop all Bots : Start all bots} + {allBotsStarted ? ( + + ) : ( + + )} ({ }, }); -export const botLoadErrorState = atomFamily({ +export const botRuntimeErrorState = atomFamily({ key: getFullyQualifiedKey('botLoadErrorMsg'), default: (id) => { return { title: '', message: '' }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts index 81c92cca33..ab89afd61e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts @@ -11,7 +11,7 @@ import { Text, BotStatus } from '../../constants'; import httpClient from '../../utils/httpUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; -import { luFilesState, qnaFilesState, dialogsState, botStatusState, botLoadErrorState } from '../atoms'; +import { luFilesState, qnaFilesState, dialogsState, botStatusState, botRuntimeErrorState } from '../atoms'; const checkEmptyQuestionOrAnswerInQnAFile = (sections) => { return sections.some((s) => !s.Answer || s.Questions.some((q) => !q.content)); @@ -43,7 +43,7 @@ export const builderDispatcher = () => { { title: Text.LUISDEPLOYFAILURE, message: '' } ); if (errorMsg.message) { - set(botLoadErrorState(projectId), errorMsg); + set(botRuntimeErrorState(projectId), errorMsg); set(botStatusState(projectId), BotStatus.failed); return; } @@ -63,7 +63,7 @@ export const builderDispatcher = () => { set(botStatusState(projectId), BotStatus.published); } catch (err) { set(botStatusState(projectId), BotStatus.failed); - set(botLoadErrorState(projectId), { + set(botRuntimeErrorState(projectId), { title: Text.LUISDEPLOYFAILURE, message: err.response?.data?.message || err.message, }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 616e4b59f4..82c9398d4d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -309,8 +309,8 @@ export const projectDispatcher = () => { } }); - const setBotStatus = useRecoilCallback<[BotStatus, string], void>( - ({ set }: CallbackInterface) => (status: BotStatus, projectId: string) => { + const setBotStatus = useRecoilCallback<[string, BotStatus], void>( + ({ set }: CallbackInterface) => (projectId: string, status: BotStatus) => { set(botStatusState(projectId), status); } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index e4ec8f5a2e..dd2585c40c 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -10,11 +10,13 @@ import { publishTypesState, botStatusState, publishHistoryState, - botLoadErrorState, + botRuntimeErrorState, isEjectRuntimeExistState, filePersistenceState, + settingsState, } from '../atoms/botState'; import { botEndpointsState } from '../atoms'; +import { openInEmulator } from '../../utils/navigation'; import { BotStatus, Text } from './../../constants'; import httpClient from './../../utils/httpUtil'; @@ -28,7 +30,7 @@ export const publisherDispatcher = () => { const publishFailure = async ({ set }: CallbackInterface, title: string, error, target, projectId: string) => { if (target.name === defaultPublishConfig.name) { set(botStatusState(projectId), BotStatus.failed); - set(botLoadErrorState(projectId), { ...error, title }); + set(botRuntimeErrorState(projectId), { ...error, title }); } // prepend the latest publish results to the history @@ -82,7 +84,7 @@ export const publisherDispatcher = () => { set(botStatusState(projectId), BotStatus.reloading); } else if (status === PUBLISH_FAILED) { set(botStatusState(projectId), BotStatus.failed); - set(botLoadErrorState(projectId), { ...data, title: formatMessage('Start bot failed') }); + set(botRuntimeErrorState(projectId), { ...data, title: formatMessage('Start bot failed') }); } } @@ -218,6 +220,23 @@ export const publisherDispatcher = () => { } ); + const openBotInEmulator = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { + const { snapshot } = callbackHelpers; + const botEndpoints = await snapshot.getPromise(botEndpointsState); + const settings = await snapshot.getPromise(settingsState(projectId)); + try { + openInEmulator( + botEndpoints[projectId] || 'http://localhost:3979/api/messages', + settings.MicrosoftAppId && settings.MicrosoftAppPassword + ? { MicrosoftAppId: settings.MicrosoftAppId, MicrosoftAppPassword: settings.MicrosoftAppPassword } + : { MicrosoftAppPassword: '', MicrosoftAppId: '' } + ); + } catch (err) { + setError(callbackHelpers, err); + logMessage(callbackHelpers, err.message); + } + }); + return { getPublishTargetTypes, publishToTarget, @@ -226,5 +245,6 @@ export const publisherDispatcher = () => { getPublishStatus, getPublishHistory, setEjectRuntimeExist, + openBotInEmulator, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 668f10a749..4419292933 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -301,6 +301,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(botDiagnosticsState(projectId), diagnostics); refreshLocalStorage(projectId, settings); + set(settingsState(projectId), mergedSettings); set(filePersistenceState(projectId), new FilePersistence(projectId)); set(undoHistoryState(projectId), new UndoHistory(projectId)); @@ -473,7 +474,6 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); - set(settingsState(rootBotProjectId), mergedSettings); const skills: { [skillId: string]: BotProjectSpaceSkill } = currentBotProjectFile.skills; diff --git a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts new file mode 100644 index 0000000000..8a8aba7ace --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { defaultPublishConfig, IPublishConfig } from '@bfc/shared'; +import { selector, selectorFamily } from 'recoil'; + +import settingsStorage from '../../utils/dialogSettingStorage'; +import { BotStatus } from '../../constants'; +import { isAbsHosted } from '../../utils/envUtil'; +import { luFilesState, qnaFilesState, settingsState } from '../atoms'; +import { Dispatcher } from '../dispatchers'; +import { dispatcherState } from '../DispatcherWrapper'; +import { isBuildConfigComplete, needsBuild } from '../../utils/buildUtil'; + +import { validateDialogSelectorFamily } from './validatedDialogs'; + +export const botBuildRequiredSelector = selectorFamily({ + key: 'botBuildRequiredSelector', + get: (projectId: string) => ({ get }) => { + const dialogs = get(validateDialogSelectorFamily(projectId)); + return !isAbsHosted() && needsBuild(dialogs); + }, +}); + +export const isBuildConfigCompleteSelector = selectorFamily({ + key: 'isBuildConfigCompleteSelector', + get: (projectId: string) => ({ get }) => { + const settings = get(settingsState(projectId)); + const config = { + luis: settings.luis, + qna: settings.qna, + }; + const dialogs = get(validateDialogSelectorFamily(projectId)); + const luFiles = get(luFilesState(projectId)); + const qnaFiles = get(qnaFilesState(projectId)); + + return { + isBuildConfigComplete: isBuildConfigComplete(config, dialogs, luFiles, qnaFiles), + config, + }; + }, +}); + +const botRuntimeAction = (dispatcher: Dispatcher) => { + return { + onStart: async (projectId: string, config?: IPublishConfig) => { + if (config) { + dispatcher.setBotStatus(projectId, BotStatus.publishing); + await dispatcher.build(config.luis, config.qna, projectId); + } + + dispatcher.setBotStatus(projectId, BotStatus.reloading); + if (config?.qna?.subscriptionKey) { + await dispatcher.setQnASettings(projectId, config.qna.subscriptionKey); + } + + const sensitiveSettings = settingsStorage.get(projectId); + await dispatcher.publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); + }, + onStop: async (projectId: string) => { + dispatcher.stopPublishBot(projectId); + }, + }; +}; + +export const botRuntimeOperationsSelector = selector({ + key: 'botRuntimeOperationsSelector', + get: ({ get }) => { + const dispatcher = get(dispatcherState); + if (!dispatcher) { + return undefined; + } + return botRuntimeAction(dispatcher); + }, +}); diff --git a/Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts b/Composer/packages/client/src/recoilModel/selectors/notifications.ts similarity index 100% rename from Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts rename to Composer/packages/client/src/recoilModel/selectors/notifications.ts diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index 8233521904..d6fd154c47 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { selector } from 'recoil'; +import { selector, selectorFamily } from 'recoil'; import isEmpty from 'lodash/isEmpty'; import { @@ -38,6 +38,7 @@ export const botProjectSpaceSelector = selector({ const botError = get(botErrorState(projectId)); const name = get(botDisplayNameState(projectId)); const botNameId = get(botNameIdentifierState(projectId)); + return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); return result; @@ -68,3 +69,20 @@ export const skillsProjectIdSelector = selector({ }); }, }); + +export const botProjectSpaceFilterSelector = selectorFamily({ + key: 'botProjectSpaceFilterSelector', + get: (filters: any) => ({ get }) => { + const botProjects = get(botProjectIdsState); + const result = botProjects.map((projectId: string) => { + const dialogs = get(dialogsState(projectId)); + const metaData = get(projectMetaDataState(projectId)); + const botError = get(botErrorState(projectId)); + const name = get(botDisplayNameState(projectId)); + const botNameId = get(botNameIdentifierState(projectId)); + + return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; + }); + return filters(result); + }, +}); diff --git a/Composer/packages/lib/shared/src/skillsUtils/index.ts b/Composer/packages/lib/shared/src/skillsUtils/index.ts index b3ba33beff..ad7b0bccdf 100644 --- a/Composer/packages/lib/shared/src/skillsUtils/index.ts +++ b/Composer/packages/lib/shared/src/skillsUtils/index.ts @@ -3,7 +3,7 @@ import get from 'lodash/get'; import keyBy from 'lodash/keyBy'; -import { BotProjectSpaceSkill, DialogSetting } from '@bfc/types'; +import { DialogSetting } from '@bfc/types'; import formatMessage from 'format-message'; import camelCase from 'lodash/camelCase'; diff --git a/Composer/packages/types/src/shell.ts b/Composer/packages/types/src/shell.ts index 442f4d0515..b38adfebd5 100644 --- a/Composer/packages/types/src/shell.ts +++ b/Composer/packages/types/src/shell.ts @@ -6,7 +6,7 @@ import type { DialogInfo, LuFile, LgFile, QnAFile, LuIntentSection, LgTemplate, import type { SkillSetting, UserSettings } from './settings'; import type { JSONSchema7 } from './schema'; -import { ManifestEndpoint, Skill } from '.'; +import { Skill } from '.'; /** Recursively marks all properties as optional. */ type AllPartial = { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx index ee526c4dab..b386b6c781 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx @@ -11,7 +11,7 @@ import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; export const ADD_DIALOG = 'ADD_DIALOG'; interface ComboBoxFieldProps { - comboboxTitle: string | null; + comboboxTitle?: string; options: IComboBoxOption[]; onChange: any; required?: boolean; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx index 4e86d983a8..00e2c1d980 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState } from 'react'; -import { IComboBoxOption, SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/ComboBox'; +import React from 'react'; +import { IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox'; import { FieldProps, useShellApi } from '@bfc/extension-client'; import formatMessage from 'format-message'; import { getSkillNameFromSetting, Skill } from '@bfc/shared'; @@ -10,8 +10,6 @@ import { Link } from 'office-ui-fabric-react/lib/components/Link/Link'; import { ComboBoxField } from './ComboBoxField'; -const ADD_DIALOG = 'ADD_DIALOG'; - const referBySettings = (skillName: string, property: string) => { return `=settings.skill['${skillName}'].${property}`; }; @@ -26,7 +24,6 @@ export const SelectSkillDialogField: React.FC = (props) => { const { shellApi, skills } = useShellApi(); const { displayManifestModal } = shellApi; - const [comboboxTitle, _] = useState(null); const skillId = getSkillNameFromSetting(value?.skillEndpoint); const { manifest, name }: Skill = skills[skillId] || {}; @@ -43,19 +40,6 @@ export const SelectSkillDialogField: React.FC = (props) => { options.push(option); } - options.push( - { - key: 'separator', - itemType: SelectableOptionMenuItemType.Divider, - text: '', - }, - { key: ADD_DIALOG, text: formatMessage('Add a new Skill Dialog') } - ); - - if (comboboxTitle) { - options.push({ key: 'customTitle', text: comboboxTitle }); - } - const handleChange = (_, option: IComboBoxOption) => { if (option) { onChange({ ...value, ...option.data }); @@ -65,7 +49,6 @@ export const SelectSkillDialogField: React.FC = (props) => { return ( Date: Tue, 13 Oct 2020 23:11:13 -0700 Subject: [PATCH 034/100] Update to settings Signed-off-by: Srinaath Ravichandran --- .../client/src/recoilModel/dispatchers/utils/project.ts | 2 +- Composer/packages/lib/indexers/src/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 4419292933..72ed81a549 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -161,7 +161,7 @@ const loadProjectData = (response) => { const mergedSettings = getMergedSettings(projectId, settings); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; - const indexedFiles = indexer.index(files, botName, locale); + const indexedFiles = indexer.index(files, botName, locale, mergedSettings); return { botFiles: { ...indexedFiles, mergedSettings }, projectData: response.data, diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index 971e69648f..066d5fa6c6 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -1,6 +1,6 @@ // 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'; @@ -45,7 +45,7 @@ class Indexer { return importResolverGenerator(lgFiles, '.lg', locale); }; - public index(files: FileInfo[], botName: string, locale: string) { + public index(files: FileInfo[], botName: string, locale: string, settings: DialogSetting) { const result = this.classifyFile(files); const luFeatures = settings.luFeatures; return { From 62629959c68c131790bc9e939495bf79d3e7a774 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 14 Oct 2020 01:10:45 -0700 Subject: [PATCH 035/100] Working bot status indicator Signed-off-by: Srinaath Ravichandran --- .../TestController/BotRuntimeOperation.tsx | 15 +++--- .../TestController/BotStatusIndicator.tsx | 3 ++ .../TestController/BotStatusText.tsx | 2 - .../TestController/TestController.tsx | 48 +++++++++---------- .../client/src/pages/design/DesignPage.tsx | 2 +- .../persistence/FilePersistence.ts | 1 - .../selectors/botRuntimeOperations.ts | 9 ++-- .../src/recoilModel/selectors/project.ts | 5 +- 8 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx b/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx index 4b830aea75..ec1307eec6 100644 --- a/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx +++ b/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx @@ -37,23 +37,26 @@ export const BotRuntimeOperation: React.FC = ({ projectI const currentBotStatus = useRecoilValue(botStatusState(projectId)); const botBuildRequired = useRecoilValue(botBuildRequiredSelector(projectId)); const { config: startBotConfig } = useRecoilValue(isBuildConfigCompleteSelector(projectId)); - const operations = useRecoilValue(botRuntimeOperationsSelector); + const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); const handleBotStop = () => { - operations?.onStop(projectId); + botRuntimeOperations?.stop(projectId); }; const handleBotStart = async () => { - let config: IPublishConfig | undefined = undefined; + const config: IPublishConfig | undefined = startBotConfig; if (botBuildRequired) { - config = startBotConfig; + // Default recognizer + botRuntimeOperations?.buildWithDefaultRecognizer(projectId, config); + } else { + // Regex recognizer + botRuntimeOperations?.startBot(projectId, config); } - operations?.onStart(projectId, config); }; return ( - {currentBotStatus === BotStatus.published ? ( + {currentBotStatus === BotStatus.connected ? ( diff --git a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx index 9eff0d36e1..db0758a0b9 100644 --- a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx +++ b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx @@ -10,6 +10,7 @@ import { SharedColors } from '@uifabric/fluent-theme'; import { BotStatus } from '../../constants'; import { botEndpointsState, botStatusState, dispatcherState } from '../../recoilModel'; +import { botRuntimeOperationsSelector } from '../../recoilModel/selectors/botRuntimeOperations'; import { EmulatorOpenButton } from './emulatorOpenButton'; @@ -29,6 +30,7 @@ export const BotStatusIndicator: React.FC = ({ projectI const botEndpoints = useRecoilValue(botEndpointsState); const { openBotInEmulator, setBotStatus, getPublishStatus } = useRecoilValue(dispatcherState); const [botStatusText, setBotStatusText] = useState(''); + const operations = useRecoilValue(botRuntimeOperationsSelector); // TODO: Build a useInterval hook instead of doing it in component. function stopPollingRuntime() { @@ -56,6 +58,7 @@ export const BotStatusIndicator: React.FC = ({ projectI break; case BotStatus.published: stopPollingRuntime(); + operations?.startBot(projectId); break; case BotStatus.reloading: startPollingRuntime(); diff --git a/Composer/packages/client/src/components/TestController/BotStatusText.tsx b/Composer/packages/client/src/components/TestController/BotStatusText.tsx index 7a7a1dbfd7..b20de14bf2 100644 --- a/Composer/packages/client/src/components/TestController/BotStatusText.tsx +++ b/Composer/packages/client/src/components/TestController/BotStatusText.tsx @@ -5,7 +5,6 @@ import { jsx, css } from '@emotion/core'; import React, { useEffect, useState } from 'react'; import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; -import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { DefaultButton, ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; @@ -19,7 +18,6 @@ import { } from 'office-ui-fabric-react/lib/DetailsList'; import { useRecoilValue } from 'recoil'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; -import { SharedColors, FontSizes } from '@uifabric/fluent-theme'; import { botProjectSpaceSelector } from '../../recoilModel'; diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index 4c4d9969ea..bc2ea01292 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -94,30 +94,30 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { } }, [projectId]); - useEffect(() => { - switch (botStatus) { - case BotStatus.failed: - openCallout(); - stopPollingRuntime(); - setBotStatus(projectId, BotStatus.pending); - break; - case BotStatus.published: - stopPollingRuntime(); - handleLoadBot(); - break; - case BotStatus.reloading: - startPollingRuntime(); - break; - default: - case BotStatus.connected: - stopPollingRuntime(); - break; - } - return () => { - stopPollingRuntime(); - return; - }; - }, [botStatus]); + // useEffect(() => { + // switch (botStatus) { + // case BotStatus.failed: + // openCallout(); + // stopPollingRuntime(); + // setBotStatus(projectId, BotStatus.pending); + // break; + // case BotStatus.published: + // stopPollingRuntime(); + // handleLoadBot(); + // break; + // case BotStatus.reloading: + // startPollingRuntime(); + // break; + // default: + // case BotStatus.connected: + // stopPollingRuntime(); + // break; + // } + // return () => { + // stopPollingRuntime(); + // return; + // }; + // }, [botStatus]); function dismissDialog() { setModalOpen(false); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 2826792d47..5a3f57c880 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -351,7 +351,7 @@ const DesignPage: React.FC { - addExistingSkillToBotProject('/Users/srravich/Desktop/LoadedBotProject/GoogleKeepSync'); + addExistingSkillToBotProject('/Users/srravich/Desktop/skill1'); }, }, { diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts index 2472d162ae..e6159e804a 100644 --- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts +++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts @@ -186,7 +186,6 @@ class FilePersistence { private getBotProjectFileChanges(current: BotProjectFile, previous: BotProjectFile) { if (!isEqual(current, previous)) { - debugger; return [ { id: `${current.id}${FileExtensions.BotProject}`, diff --git a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts index 8a8aba7ace..fe41a7f9ef 100644 --- a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts +++ b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts @@ -43,21 +43,22 @@ export const isBuildConfigCompleteSelector = selectorFamily({ const botRuntimeAction = (dispatcher: Dispatcher) => { return { - onStart: async (projectId: string, config?: IPublishConfig) => { + buildWithDefaultRecognizer: async (projectId: string, config: IPublishConfig) => { if (config) { dispatcher.setBotStatus(projectId, BotStatus.publishing); await dispatcher.build(config.luis, config.qna, projectId); } - + }, + startBot: async (projectId, config?: IPublishConfig) => { dispatcher.setBotStatus(projectId, BotStatus.reloading); - if (config?.qna?.subscriptionKey) { + if (config?.qna?.subscriptionKey && !config?.qna?.endpointKey) { await dispatcher.setQnASettings(projectId, config.qna.subscriptionKey); } const sensitiveSettings = settingsStorage.get(projectId); await dispatcher.publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); }, - onStop: async (projectId: string) => { + stop: async (projectId: string) => { dispatcher.stopPublishBot(projectId); }, }; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index d6fd154c47..ffbf5833dc 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -12,6 +12,7 @@ import { dialogsState, projectMetaDataState, botNameIdentifierState, + botStatusState, } from '../atoms'; // Actions @@ -38,9 +39,11 @@ export const botProjectSpaceSelector = selector({ const botError = get(botErrorState(projectId)); const name = get(botDisplayNameState(projectId)); const botNameId = get(botNameIdentifierState(projectId)); + const botStatus = get(botStatusState(projectId)); - return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; + return { dialogs, projectId, name, ...metaData, error: botError, botNameId, botStatus }; }); + console.log('RESULT', result); return result; }, }); From d000d764b7f7137a14a0e4aa1c0012c6fc6313fd Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 14 Oct 2020 20:48:51 -0700 Subject: [PATCH 036/100] Update to start component Signed-off-by: Srinaath Ravichandran --- .../packages/client/src/components/Header.tsx | 21 +++++- ...Operation.tsx => BotRuntimeOperations.tsx} | 10 +-- .../TestController/BotStatusIndicator.tsx | 71 ++++++++----------- .../TestController/startBotsDialog.tsx | 67 +++++++++++++++-- Composer/packages/client/src/constants.ts | 9 +++ .../src/recoilModel/DispatcherWrapper.tsx | 6 +- .../selectors/botRuntimeOperations.ts | 62 +++++++++++++--- .../client/src/recoilModel/selectors/index.ts | 1 + .../src/recoilModel/selectors/project.ts | 28 ++------ Composer/packages/client/src/utils/hooks.ts | 20 ++++++ 10 files changed, 201 insertions(+), 94 deletions(-) rename Composer/packages/client/src/components/TestController/{BotRuntimeOperation.tsx => BotRuntimeOperations.tsx} (86%) diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index d17692e553..43da1354a3 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -5,7 +5,7 @@ import { jsx, css } from '@emotion/core'; import formatMessage from 'format-message'; import { IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; -import { useCallback, Fragment, useState } from 'react'; +import { useCallback, Fragment, useState, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { SharedColors } from '@uifabric/fluent-theme'; import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; @@ -16,6 +16,7 @@ import { botDisplayNameState, localeState, currentProjectIdState, + runningBotsSelector, } from '../recoilModel'; import composerIcon from '../images/composerIcon.svg'; import { AppUpdaterStatus } from '../constants'; @@ -79,7 +80,7 @@ const headerTextContainer = css` `; // -------------------- Header -------------------- // - +const defaultStartPanelText = formatMessage('Start all bots'); export const Header = () => { const { setAppUpdateShowing } = useRecoilValue(dispatcherState); const projectId = useRecoilValue(currentProjectIdState); @@ -88,6 +89,16 @@ export const Header = () => { const appUpdate = useRecoilValue(appUpdateState); const { showing, status } = appUpdate; const [showStartBotsPanel, setStartPanelView] = useState(false); + const runningBots = useRecoilValue(runningBotsSelector); + const [startPanelText, setStartPanelText] = useState('Start '); + + useEffect(() => { + if (runningBots.totalBots === 0) { + setStartPanelText(`Start all (${runningBots.projectIds.length}/${runningBots.totalBots} running)`); + } else { + setStartPanelText(`Stop all bots`); + } + }, [runningBots]); const onUpdateAvailableClick = useCallback(() => { setAppUpdateShowing(true); @@ -128,8 +139,12 @@ export const Header = () => { onClick={onUpdateAvailableClick} /> )} - + + {showStartBotsPanel && }
); }; +//?Start Bots diff --git a/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx b/Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx similarity index 86% rename from Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx rename to Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx index ec1307eec6..af5969bc3b 100644 --- a/Composer/packages/client/src/components/TestController/BotRuntimeOperation.tsx +++ b/Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx @@ -15,10 +15,10 @@ import { BotStatus } from '../../constants'; import { botBuildRequiredSelector, botRuntimeOperationsSelector, - isBuildConfigCompleteSelector, + buildEssentialsSelector, } from '../../recoilModel/selectors/botRuntimeOperations'; -interface BotStartOperationProps { +interface BotRuntimeOperationsProps { projectId: string; displayName: string; } @@ -33,14 +33,14 @@ const icon: IButtonStyles = { }, }; -export const BotRuntimeOperation: React.FC = ({ projectId, displayName }) => { +export const BotRuntimeOperations: React.FC = ({ projectId, displayName }) => { const currentBotStatus = useRecoilValue(botStatusState(projectId)); const botBuildRequired = useRecoilValue(botBuildRequiredSelector(projectId)); - const { config: startBotConfig } = useRecoilValue(isBuildConfigCompleteSelector(projectId)); + const { configuration: startBotConfig } = useRecoilValue(buildEssentialsSelector(projectId)); const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); const handleBotStop = () => { - botRuntimeOperations?.stop(projectId); + botRuntimeOperations?.stopBot(projectId); }; const handleBotStart = async () => { diff --git a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx index db0758a0b9..3ca72f1a61 100644 --- a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx +++ b/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx @@ -6,86 +6,71 @@ import { jsx } from '@emotion/core'; import { Fragment, useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { defaultPublishConfig } from '@bfc/shared'; -import { SharedColors } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; -import { BotStatus } from '../../constants'; +import { BotStatus, BotStatusesCopy } from '../../constants'; import { botEndpointsState, botStatusState, dispatcherState } from '../../recoilModel'; import { botRuntimeOperationsSelector } from '../../recoilModel/selectors/botRuntimeOperations'; +import { useInterval } from '../../utils/hooks'; import { EmulatorOpenButton } from './emulatorOpenButton'; const POLLING_INTERVAL = 2500; -let botStatusInterval: NodeJS.Timeout | undefined = undefined; interface BotStatusIndicatorProps { projectId: string; } -const statusStyle = { - color: SharedColors.green20, -}; - export const BotStatusIndicator: React.FC = ({ projectId }) => { const currentBotStatus = useRecoilValue(botStatusState(projectId)); const botEndpoints = useRecoilValue(botEndpointsState); const { openBotInEmulator, setBotStatus, getPublishStatus } = useRecoilValue(dispatcherState); const [botStatusText, setBotStatusText] = useState(''); const operations = useRecoilValue(botRuntimeOperationsSelector); + const [isRunning, setIntervalRunning] = useState(false); - // TODO: Build a useInterval hook instead of doing it in component. - function stopPollingRuntime() { - if (botStatusInterval) { - clearInterval(botStatusInterval); - botStatusInterval = undefined; - } - } - - function startPollingRuntime() { - if (!botStatusInterval) { - const cancelInterval = setInterval(() => { - // get publish status - getPublishStatus(projectId, defaultPublishConfig); - }, POLLING_INTERVAL); - botStatusInterval = cancelInterval; - } - } + useInterval( + () => { + getPublishStatus(projectId, defaultPublishConfig); + }, + isRunning ? POLLING_INTERVAL : null + ); useEffect(() => { switch (currentBotStatus) { case BotStatus.failed: - stopPollingRuntime(); - setBotStatus(projectId, BotStatus.pending); + setBotStatusText(BotStatusesCopy[BotStatus.failed]); + setIntervalRunning(false); break; case BotStatus.published: - stopPollingRuntime(); + setBotStatusText(BotStatusesCopy[BotStatus.published]); + setIntervalRunning(false); operations?.startBot(projectId); break; case BotStatus.reloading: - startPollingRuntime(); + setBotStatusText(BotStatusesCopy[BotStatus.reloading]); + setIntervalRunning(true); break; - default: + case BotStatus.connected: - stopPollingRuntime(); + setIntervalRunning(false); + setBotStatusText(BotStatusesCopy[BotStatus.connected]); break; - } - if (currentBotStatus === BotStatus.connected) { - setBotStatusText('Started'); - } else if (currentBotStatus === BotStatus.failed) { - setBotStatusText('Error occured'); - } else { - setBotStatusText('Inactive'); - } + case BotStatus.publishing: + setBotStatusText(BotStatusesCopy[BotStatus.publishing]); + break; - return () => { - stopPollingRuntime(); - return; - }; + default: + case BotStatus.unConnected: + setBotStatusText(BotStatusesCopy[BotStatus.unConnected]); + break; + } }, [currentBotStatus]); return ( - {botStatusText} + {botStatusText} void, trackedProjectIds: string[]) { + const savedCallback: any = useRef(); + const areBotsStarting = useRecoilValue(trackBotStatusesSelector(trackedProjectIds)); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = postSkillsStartAction; + }, [postSkillsStartAction]); -// eslint-disable-next-line react/display-name + useEffect(() => { + const trackedBotsStarted = !areBotsStarting; + if (trackedProjectIds.length && trackedBotsStarted) { + // Start the root bot now after skills are started. + savedCallback.current(); + } + }, [areBotsStarting]); +} interface IStartBotsDialogProps { isOpen: boolean; @@ -60,21 +79,56 @@ export const StartBotsDialog: React.FC = (props) => { const projectCollection = useRecoilValue(botProjectSpaceSelector); const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); const [allBotsStarted, setAllBotsStarted] = useState(false); + const builderEssentials = useRecoilValue(buildConfigurationSelector); + const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); + const [trackedProjectIds, setProjectsToTrack] = useState([]); useEffect(() => { const result = projectCollection.map(({ name, projectId }) => ({ displayName: name, projectId })); setItems(result); }, [projectCollection]); + const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { + if (botBuildRequired) { + // Default recognizer + botRuntimeOperations?.buildWithDefaultRecognizer(projectId, config); + } else { + // Regex recognizer + botRuntimeOperations?.startBot(projectId, config); + } + }; + + const startRootBot = () => { + setProjectsToTrack([]); + const rootBot = builderEssentials[0]; + const { projectId, configuration, buildRequired } = rootBot; + handleBotStart(projectId, configuration, buildRequired); + }; + + // Custom hook to make sure root bot is started after all skills have been started. + useBotStatusTracker(() => { + startRootBot(); + }, trackedProjectIds); + const onRenderRow = (props?: IDetailsRowProps, defaultRender?: IRenderFunction): JSX.Element => { return
{defaultRender && defaultRender(props)}
; }; const startAllBots = () => { + const [_, ...skillsBots] = builderEssentials; + const trackProjects: string[] = []; + for (const botBuildConfig of skillsBots) { + const { projectId, configuration, buildRequired } = botBuildConfig; + trackProjects.push(projectId); + handleBotStart(projectId, configuration, buildRequired); + } + setProjectsToTrack(trackProjects); setAllBotsStarted(true); }; const stopAllBots = () => { + setProjectsToTrack([]); + builderEssentials.forEach(({ projectId }) => botRuntimeOperations?.stopBot(projectId)); setAllBotsStarted(false); }; @@ -90,7 +144,7 @@ export const StartBotsDialog: React.FC = (props) => { sortDescendingAriaLabel: formatMessage('Sorted Z to A'), data: 'string', onRender: (item: { displayName: string; projectId: string }) => { - return ; + return ; }, isPadded: true, }, @@ -134,7 +188,6 @@ export const StartBotsDialog: React.FC = (props) => { )} - => { const result = await Promise.all([ @@ -88,10 +88,10 @@ const InitDispatcher = ({ onLoad }) => { export const DispatcherWrapper = ({ children }) => { const [loaded, setLoaded] = useState(false); - const botProjects = useRecoilValue(botsForFilePersistenceSelector); + const botProjects = useRecoilValue(localBotsWithoutErrorsSelector); useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => { - const botsForFilePersistence = await snapshot.getPromise(botsForFilePersistenceSelector); + const botsForFilePersistence = await snapshot.getPromise(localBotsWithoutErrorsSelector); for (const projectId of botsForFilePersistence) { const assets = await getBotAssets(projectId, snapshot); const previousAssets = await getBotAssets(projectId, previousSnapshot); diff --git a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts index fe41a7f9ef..50d6942eee 100644 --- a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts +++ b/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts @@ -7,13 +7,28 @@ import { selector, selectorFamily } from 'recoil'; import settingsStorage from '../../utils/dialogSettingStorage'; import { BotStatus } from '../../constants'; import { isAbsHosted } from '../../utils/envUtil'; -import { luFilesState, qnaFilesState, settingsState } from '../atoms'; +import { botStatusState, luFilesState, qnaFilesState, settingsState } from '../atoms'; +import { localBotsWithoutErrorsSelector } from '../selectors'; import { Dispatcher } from '../dispatchers'; import { dispatcherState } from '../DispatcherWrapper'; -import { isBuildConfigComplete, needsBuild } from '../../utils/buildUtil'; +import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil'; import { validateDialogSelectorFamily } from './validatedDialogs'; +export const trackBotStatusesSelector = selectorFamily({ + key: 'trackBotStatusesSelector', + get: (trackedProjectIds: string[]) => ({ get }) => { + if (trackedProjectIds.length === 0) { + return false; + } + const areBotsRunning = trackedProjectIds.find((projectId: string) => { + const currentStatus = get(botStatusState(projectId)); + return currentStatus !== BotStatus.connected && currentStatus !== BotStatus.failed; + }); + return areBotsRunning; + }, +}); + export const botBuildRequiredSelector = selectorFamily({ key: 'botBuildRequiredSelector', get: (projectId: string) => ({ get }) => { @@ -22,21 +37,50 @@ export const botBuildRequiredSelector = selectorFamily({ }, }); -export const isBuildConfigCompleteSelector = selectorFamily({ - key: 'isBuildConfigCompleteSelector', +export const buildEssentialsSelector = selectorFamily({ + key: 'buildEssentialsSelector', get: (projectId: string) => ({ get }) => { const settings = get(settingsState(projectId)); - const config = { + const configuration = { luis: settings.luis, qna: settings.qna, }; const dialogs = get(validateDialogSelectorFamily(projectId)); const luFiles = get(luFilesState(projectId)); const qnaFiles = get(qnaFilesState(projectId)); + const botBuildRequired = get(botBuildRequiredSelector(projectId)); + + return { + isConfigurationComplete: isBuildConfigurationComplete(configuration, dialogs, luFiles, qnaFiles), + configuration, + buildRequired: botBuildRequired, + projectId, + }; + }, +}); +export const buildConfigurationSelector = selector({ + key: 'buildConfigurationSelector', + get: ({ get }) => { + const localProjects = get(localBotsWithoutErrorsSelector); + return localProjects.map((projectId: string) => { + const result = get(buildEssentialsSelector(projectId)); + return result; + }); + }, +}); + +export const runningBotsSelector = selector({ + key: 'runningBotsSelector', + get: ({ get }) => { + const localProjects = get(localBotsWithoutErrorsSelector); + const botsRunning = localProjects.filter((projectId: string) => { + const result = get(botStatusState(projectId)); + return result === BotStatus.connected; + }); return { - isBuildConfigComplete: isBuildConfigComplete(config, dialogs, luFiles, qnaFiles), - config, + totalBots: localProjects.length, + projectIds: botsRunning, }; }, }); @@ -49,7 +93,7 @@ const botRuntimeAction = (dispatcher: Dispatcher) => { await dispatcher.build(config.luis, config.qna, projectId); } }, - startBot: async (projectId, config?: IPublishConfig) => { + startBot: async (projectId: string, config?: IPublishConfig) => { dispatcher.setBotStatus(projectId, BotStatus.reloading); if (config?.qna?.subscriptionKey && !config?.qna?.endpointKey) { await dispatcher.setQnASettings(projectId, config.qna.subscriptionKey); @@ -58,7 +102,7 @@ const botRuntimeAction = (dispatcher: Dispatcher) => { const sensitiveSettings = settingsStorage.get(projectId); await dispatcher.publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); }, - stop: async (projectId: string) => { + stopBot: async (projectId: string) => { dispatcher.stopPublishBot(projectId); }, }; diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index f823d56f75..91b7f443ed 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -6,3 +6,4 @@ export * from './eject'; export * from './extensions'; export * from './validatedDialogs'; export * from './skills'; +export * from './botRuntimeOperations'; diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index ffbf5833dc..cb66fab909 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { selector, selectorFamily } from 'recoil'; +import { selector } from 'recoil'; import isEmpty from 'lodash/isEmpty'; import { @@ -12,12 +12,11 @@ import { dialogsState, projectMetaDataState, botNameIdentifierState, - botStatusState, } from '../atoms'; // Actions -export const botsForFilePersistenceSelector = selector({ - key: 'botsForFilePersistenceSelector', +export const localBotsWithoutErrorsSelector = selector({ + key: 'localBotsWithoutErrorsSelector', get: ({ get }) => { const botProjectIds = get(botProjectIdsState); return botProjectIds.filter((projectId: string) => { @@ -39,11 +38,9 @@ export const botProjectSpaceSelector = selector({ const botError = get(botErrorState(projectId)); const name = get(botDisplayNameState(projectId)); const botNameId = get(botNameIdentifierState(projectId)); - const botStatus = get(botStatusState(projectId)); - return { dialogs, projectId, name, ...metaData, error: botError, botNameId, botStatus }; + return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); - console.log('RESULT', result); return result; }, }); @@ -72,20 +69,3 @@ export const skillsProjectIdSelector = selector({ }); }, }); - -export const botProjectSpaceFilterSelector = selectorFamily({ - key: 'botProjectSpaceFilterSelector', - get: (filters: any) => ({ get }) => { - const botProjects = get(botProjectIdsState); - const result = botProjects.map((projectId: string) => { - const dialogs = get(dialogsState(projectId)); - const metaData = get(projectMetaDataState(projectId)); - const botError = get(botErrorState(projectId)); - const name = get(botDisplayNameState(projectId)); - const botNameId = get(botNameIdentifierState(projectId)); - - return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; - }); - return filters(result); - }, -}); diff --git a/Composer/packages/client/src/utils/hooks.ts b/Composer/packages/client/src/utils/hooks.ts index c24bc5e8b7..fb88a30466 100644 --- a/Composer/packages/client/src/utils/hooks.ts +++ b/Composer/packages/client/src/utils/hooks.ts @@ -69,3 +69,23 @@ export const useProjectIdCache = () => { return projectId; }; + +export function useInterval(callback, delay) { + const savedCallback: any = useRef(); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + function tick() { + savedCallback.current(); + } + if (delay !== null) { + const id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); +} From e2041ee534fba463e61709c8b59c5c5da15529ef Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Thu, 15 Oct 2020 23:35:24 -0700 Subject: [PATCH 037/100] Stable LocalBotRuntime operations Signed-off-by: Srinaath Ravichandran --- .../packages/client/src/components/Header.tsx | 105 ++++++--- ...timeOperations.tsx => LocalBotRuntime.tsx} | 11 +- ...icator.tsx => LocalBotStatusIndicator.tsx} | 69 +++++- .../TestController/startBotsDialog.tsx | 207 ------------------ .../{BotStatusText.tsx => startBotsPanel.tsx} | 80 ++++--- .../TestController/useBotStatusTracker.ts | 25 +++ .../TestController/useLocalBotOperations.ts | 73 ++++++ .../recoilModel/dispatchers/botProjectFile.ts | 8 +- .../recoilModel/dispatchers/utils/project.ts | 5 +- .../client/src/recoilModel/selectors/index.ts | 2 +- ...meOperations.ts => localRuntimeBuilder.ts} | 3 +- .../shared/__tests__/fileUtils/index.test.ts | 6 +- Composer/plugins/localPublish/package.json | 2 + Composer/plugins/localPublish/yarn.lock | 23 ++ 14 files changed, 327 insertions(+), 292 deletions(-) rename Composer/packages/client/src/components/TestController/{BotRuntimeOperations.tsx => LocalBotRuntime.tsx} (88%) rename Composer/packages/client/src/components/TestController/{BotStatusIndicator.tsx => LocalBotStatusIndicator.tsx} (54%) delete mode 100644 Composer/packages/client/src/components/TestController/startBotsDialog.tsx rename Composer/packages/client/src/components/TestController/{BotStatusText.tsx => startBotsPanel.tsx} (69%) create mode 100644 Composer/packages/client/src/components/TestController/useBotStatusTracker.ts create mode 100644 Composer/packages/client/src/components/TestController/useLocalBotOperations.ts rename Composer/packages/client/src/recoilModel/selectors/{botRuntimeOperations.ts => localRuntimeBuilder.ts} (98%) diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index 43da1354a3..f56b716bd1 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -4,11 +4,12 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; import formatMessage from 'format-message'; -import { IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { IconButton, IButtonStyles, ActionButton } from 'office-ui-fabric-react/lib/Button'; import { useCallback, Fragment, useState, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; -import { SharedColors } from '@uifabric/fluent-theme'; import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; import { dispatcherState, @@ -21,7 +22,13 @@ import { import composerIcon from '../images/composerIcon.svg'; import { AppUpdaterStatus } from '../constants'; -import { StartBotsDialog } from './TestController/startBotsDialog'; +import { StartBotsPanel } from './TestController/startBotsPanel'; +import { useBotOperations } from './TestController/useLocalBotOperations'; +export const actionButton = css` + font-size: 18px; + margin-top: 2px; + color: #fff; +`; // -------------------- Styles -------------------- // @@ -59,11 +66,9 @@ const updateAvailableIcon = { fontSize: '20px', }, root: { - position: 'absolute', height: '20px', width: '20px', - top: 'calc(50% - 10px)', - right: '20px', + margin: '0 20px', }, rootHovered: { backgroundColor: 'transparent', @@ -75,12 +80,30 @@ const updateAvailableIcon = { const headerTextContainer = css` display: flex; - flex-direction: row; - flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + width: 50%; `; +const rightSection = css` + display: flex; + align-items: center; + justify-content: flex-end; + width: 50%; +`; + +const botRuntimeStartIcon: IButtonStyles = { + root: { + color: `#fff`, + marginRight: '12px', + boxSizing: 'border-box', + fontSize: `${FontSizes.size16}`, + width: '20px', + }, +}; + // -------------------- Header -------------------- // -const defaultStartPanelText = formatMessage('Start all bots'); + export const Header = () => { const { setAppUpdateShowing } = useRecoilValue(dispatcherState); const projectId = useRecoilValue(currentProjectIdState); @@ -90,13 +113,19 @@ export const Header = () => { const { showing, status } = appUpdate; const [showStartBotsPanel, setStartPanelView] = useState(false); const runningBots = useRecoilValue(runningBotsSelector); - const [startPanelText, setStartPanelText] = useState('Start '); + const [startPanelText, setStartPanelText] = useState(''); + const [allBotsStarted, setAllBotsStarted] = useState(false); + const { stopAllBots, startAllBots } = useBotOperations(setAllBotsStarted); useEffect(() => { - if (runningBots.totalBots === 0) { - setStartPanelText(`Start all (${runningBots.projectIds.length}/${runningBots.totalBots} running)`); + if (runningBots.projectIds.length > 0) { + setStartPanelText( + `${formatMessage('Stop all bots')} (${runningBots.projectIds.length}/${runningBots.totalBots}) ${formatMessage( + 'running' + )})` + ); } else { - setStartPanelText(`Stop all bots`); + setStartPanelText(formatMessage('Start all bots')); } }, [runningBots]); @@ -108,8 +137,15 @@ export const Header = () => { setStartPanelView(false); } - function showStartPanelViewer() { - setStartPanelView(true); + function handleStartOrStopAll() { + if (allBotsStarted) { + stopAllBots(); + dismissStartPanelViewer(); + setAllBotsStarted(false); + } else { + startAllBots(); + setStartPanelView(true); + } } const showUpdateAvailableIcon = status === AppUpdaterStatus.UPDATE_AVAILABLE && !showing; @@ -131,19 +167,32 @@ export const Header = () => {
)}
- {showUpdateAvailableIcon && ( - - )} - - - {showStartBotsPanel && } + +
+ {runningBots.projectIds.length > 0 ? ( + + + + ) : ( + + + + )} + setStartPanelView(true)}> + {startPanelText} + + + {showUpdateAvailableIcon && ( + + )} +
+ + {showStartBotsPanel && }
); }; diff --git a/Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx similarity index 88% rename from Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx rename to Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx index af5969bc3b..0c30639192 100644 --- a/Composer/packages/client/src/components/TestController/BotRuntimeOperations.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx @@ -3,7 +3,6 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { Fragment } from 'react'; import { useRecoilValue } from 'recoil'; import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; @@ -16,9 +15,9 @@ import { botBuildRequiredSelector, botRuntimeOperationsSelector, buildEssentialsSelector, -} from '../../recoilModel/selectors/botRuntimeOperations'; +} from '../../recoilModel/selectors/localRuntimeBuilder'; -interface BotRuntimeOperationsProps { +interface LocalBotRuntimeProps { projectId: string; displayName: string; } @@ -33,7 +32,7 @@ const icon: IButtonStyles = { }, }; -export const BotRuntimeOperations: React.FC = ({ projectId, displayName }) => { +export const LocalBotRuntime: React.FC = ({ projectId, displayName }) => { const currentBotStatus = useRecoilValue(botStatusState(projectId)); const botBuildRequired = useRecoilValue(botBuildRequiredSelector(projectId)); const { configuration: startBotConfig } = useRecoilValue(buildEssentialsSelector(projectId)); @@ -55,7 +54,7 @@ export const BotRuntimeOperations: React.FC = ({ proj }; return ( - +
{currentBotStatus === BotStatus.connected ? ( )} {displayName} - +
); }; diff --git a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx similarity index 54% rename from Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx rename to Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx index 3ca72f1a61..f97a58352f 100644 --- a/Composer/packages/client/src/components/TestController/BotStatusIndicator.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx @@ -2,32 +2,49 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { Fragment, useEffect, useState } from 'react'; +import { jsx, css } from '@emotion/core'; +import { useEffect, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { defaultPublishConfig } from '@bfc/shared'; +import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import formatMessage from 'format-message'; import { BotStatus, BotStatusesCopy } from '../../constants'; -import { botEndpointsState, botStatusState, dispatcherState } from '../../recoilModel'; -import { botRuntimeOperationsSelector } from '../../recoilModel/selectors/botRuntimeOperations'; +import { botEndpointsState, botRuntimeErrorState, botStatusState, dispatcherState } from '../../recoilModel'; +import { botRuntimeOperationsSelector } from '../../recoilModel/selectors/localRuntimeBuilder'; import { useInterval } from '../../utils/hooks'; import { EmulatorOpenButton } from './emulatorOpenButton'; +import { ErrorCallout } from './errorCallout'; +import { useBotOperations } from './useLocalBotOperations'; + +const actionButton = css` + font-size: 18px; + color: #0078d4; +`; + +const botStatusContainer = css` + display: flex; + align-items: center; +`; const POLLING_INTERVAL = 2500; -interface BotStatusIndicatorProps { +interface LocalBotStatusIndicatorProps { projectId: string; } -export const BotStatusIndicator: React.FC = ({ projectId }) => { +export const LocalBotStatusIndicator: React.FC = ({ projectId }) => { const currentBotStatus = useRecoilValue(botStatusState(projectId)); const botEndpoints = useRecoilValue(botEndpointsState); - const { openBotInEmulator, setBotStatus, getPublishStatus } = useRecoilValue(dispatcherState); + const { openBotInEmulator, getPublishStatus } = useRecoilValue(dispatcherState); const [botStatusText, setBotStatusText] = useState(''); const operations = useRecoilValue(botRuntimeOperationsSelector); const [isRunning, setIntervalRunning] = useState(false); + const botActionRef = useRef(null); + const botLoadErrorMsg = useRecoilValue(botRuntimeErrorState(projectId)); + const [calloutVisible, setErrorCallout] = useState(false); + const { startSingleBot } = useBotOperations(undefined); useInterval( () => { @@ -36,6 +53,14 @@ export const BotStatusIndicator: React.FC = ({ projectI isRunning ? POLLING_INTERVAL : null ); + function dismissErrorDialog() { + setErrorCallout(false); + } + + function openErrorDialog() { + setErrorCallout(true); + } + useEffect(() => { switch (currentBotStatus) { case BotStatus.failed: @@ -66,10 +91,19 @@ export const BotStatusIndicator: React.FC = ({ projectI setBotStatusText(BotStatusesCopy[BotStatus.unConnected]); break; } + + return () => { + dismissErrorDialog(); + }; }, [currentBotStatus]); + const onTryStartAgain = () => { + dismissErrorDialog(); + startSingleBot(projectId); + }; + return ( - +
{botStatusText} = ({ projectI hidden={false} onClick={() => openBotInEmulator(projectId)} /> - + {botLoadErrorMsg?.message && ( + { + openErrorDialog(); + }} + > + {formatMessage('See details')} + + )} + +
); }; diff --git a/Composer/packages/client/src/components/TestController/startBotsDialog.tsx b/Composer/packages/client/src/components/TestController/startBotsDialog.tsx deleted file mode 100644 index ce3d99140c..0000000000 --- a/Composer/packages/client/src/components/TestController/startBotsDialog.tsx +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx, css } from '@emotion/core'; -import React, { useEffect, useRef, useState } from 'react'; -import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; -import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; -import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; -import { DefaultButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; -import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; -import formatMessage from 'format-message'; -import { - DetailsList, - DetailsListLayoutMode, - IDetailsRowProps, - SelectionMode, -} from 'office-ui-fabric-react/lib/DetailsList'; -import { useRecoilValue } from 'recoil'; -import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; -import { IPublishConfig } from '@bfc/shared'; - -import { botProjectSpaceSelector } from '../../recoilModel'; -import { - buildConfigurationSelector, - botRuntimeOperationsSelector, - trackBotStatusesSelector, -} from '../../recoilModel/selectors'; - -import { BotStatusIndicator } from './BotStatusIndicator'; -import { BotRuntimeOperations } from './BotRuntimeOperations'; - -const styles = { - detailListContainer: css` - flex-grow: 1; - height: 350px; - position: relative; - overflow: hidden; - `, -}; - -export const actionButton = css` - font-size: 14px; - margin-top: 2px; - color: #0078d4; -`; - -const dialog = { - title: { - fontWeight: FontWeights.bold, - }, -}; - -function useBotStatusTracker(postSkillsStartAction: () => void, trackedProjectIds: string[]) { - const savedCallback: any = useRef(); - const areBotsStarting = useRecoilValue(trackBotStatusesSelector(trackedProjectIds)); - - // Remember the latest callback. - useEffect(() => { - savedCallback.current = postSkillsStartAction; - }, [postSkillsStartAction]); - - useEffect(() => { - const trackedBotsStarted = !areBotsStarting; - if (trackedProjectIds.length && trackedBotsStarted) { - // Start the root bot now after skills are started. - savedCallback.current(); - } - }, [areBotsStarting]); -} - -interface IStartBotsDialogProps { - isOpen: boolean; - onDismiss: () => void; -} - -export const StartBotsDialog: React.FC = (props) => { - const { isOpen, onDismiss } = props; - const projectCollection = useRecoilValue(botProjectSpaceSelector); - const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); - const [allBotsStarted, setAllBotsStarted] = useState(false); - const builderEssentials = useRecoilValue(buildConfigurationSelector); - const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); - const [trackedProjectIds, setProjectsToTrack] = useState([]); - - useEffect(() => { - const result = projectCollection.map(({ name, projectId }) => ({ displayName: name, projectId })); - setItems(result); - }, [projectCollection]); - - const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { - if (botBuildRequired) { - // Default recognizer - botRuntimeOperations?.buildWithDefaultRecognizer(projectId, config); - } else { - // Regex recognizer - botRuntimeOperations?.startBot(projectId, config); - } - }; - - const startRootBot = () => { - setProjectsToTrack([]); - const rootBot = builderEssentials[0]; - const { projectId, configuration, buildRequired } = rootBot; - handleBotStart(projectId, configuration, buildRequired); - }; - - // Custom hook to make sure root bot is started after all skills have been started. - useBotStatusTracker(() => { - startRootBot(); - }, trackedProjectIds); - - const onRenderRow = (props?: IDetailsRowProps, defaultRender?: IRenderFunction): JSX.Element => { - return
{defaultRender && defaultRender(props)}
; - }; - - const startAllBots = () => { - const [_, ...skillsBots] = builderEssentials; - const trackProjects: string[] = []; - for (const botBuildConfig of skillsBots) { - const { projectId, configuration, buildRequired } = botBuildConfig; - trackProjects.push(projectId); - handleBotStart(projectId, configuration, buildRequired); - } - setProjectsToTrack(trackProjects); - setAllBotsStarted(true); - }; - - const stopAllBots = () => { - setProjectsToTrack([]); - builderEssentials.forEach(({ projectId }) => botRuntimeOperations?.stopBot(projectId)); - setAllBotsStarted(false); - }; - - const tableColumns: any = [ - { - key: 'botName', - name: formatMessage('Bot'), - fieldName: 'id', - isRowHeader: true, - isResizable: true, - isSortedDescending: false, - sortAscendingAriaLabel: formatMessage('Sorted A to Z'), - sortDescendingAriaLabel: formatMessage('Sorted Z to A'), - data: 'string', - onRender: (item: { displayName: string; projectId: string }) => { - return ; - }, - isPadded: true, - }, - { - key: 'status', - name: formatMessage('Status'), - fieldName: 'type', - isRowHeader: true, - isResizable: true, - isSorted: true, - data: 'string', - onRender: (item: { displayName: string; projectId: string }) => { - return ; - }, - isPadded: true, - }, - ]; - - return ( - - ); -}; diff --git a/Composer/packages/client/src/components/TestController/BotStatusText.tsx b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx similarity index 69% rename from Composer/packages/client/src/components/TestController/BotStatusText.tsx rename to Composer/packages/client/src/components/TestController/startBotsPanel.tsx index b20de14bf2..25eca94728 100644 --- a/Composer/packages/client/src/components/TestController/BotStatusText.tsx +++ b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx @@ -5,9 +5,9 @@ import { jsx, css } from '@emotion/core'; import React, { useEffect, useState } from 'react'; import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; +import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; -import { Icon } from 'office-ui-fabric-react/lib/Icon'; -import { DefaultButton, ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; import formatMessage from 'format-message'; import { @@ -21,17 +21,45 @@ import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; import { botProjectSpaceSelector } from '../../recoilModel'; -interface IStartBotsDialogProps { +import { LocalBotStatusIndicator } from './LocalBotStatusIndicator'; +import { LocalBotRuntime } from './LocalBotRuntime'; + +const styles = { + detailListContainer: css` + flex-grow: 1; + height: 350px; + position: relative; + overflow: hidden; + `, +}; + +export const actionButton = css` + font-size: 14px; + margin-top: 2px; + color: #0078d4; +`; + +export const botInfoContainer = css` + display: flex; + align-items: center; + justify-content: flex-start; +`; + +const dialog = { + title: { + fontWeight: FontWeights.bold, + }, +}; + +interface StartBotsPanelProps { isOpen: boolean; onDismiss: () => void; } -export const StartBotsDialog: React.FC = (props) => { +export const StartBotsPanel: React.FC = (props) => { const { isOpen, onDismiss } = props; const projectCollection = useRecoilValue(botProjectSpaceSelector); - const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); - const [allBotsStarted, setAllBotsStarted] = useState(false); useEffect(() => { const result = projectCollection.map(({ name, projectId }) => ({ displayName: name, projectId })); @@ -42,18 +70,12 @@ export const StartBotsDialog: React.FC = (props) => { return
{defaultRender && defaultRender(props)}
; }; - const startAllBots = () => { - setAllBotsStarted(true); - }; - - const stopAllBots = () => { - setAllBotsStarted(false); - }; - - const tableColumns = [ + const tableColumns: any = [ { - key: 'column1', + key: 'botName', name: formatMessage('Bot'), + minWidth: 200, + maxWidth: 200, fieldName: 'id', isRowHeader: true, isResizable: true, @@ -61,29 +83,28 @@ export const StartBotsDialog: React.FC = (props) => { sortAscendingAriaLabel: formatMessage('Sorted A to Z'), sortDescendingAriaLabel: formatMessage('Sorted Z to A'), data: 'string', - onRender: (item: any) => { + onRender: (item: { displayName: string; projectId: string }) => { return ( -
- - {item.displayName} +
+
); }, isPadded: true, }, { - key: 'column2', + key: 'status', name: formatMessage('Status'), fieldName: 'type', isRowHeader: true, isResizable: true, isSorted: true, data: 'string', - onRender: (item: any) => { + onRender: (item: { displayName: string; projectId: string }) => { return ( - - {item.status} - +
+ +
); }, isPadded: true, @@ -108,15 +129,6 @@ export const StartBotsDialog: React.FC = (props) => { >
- - {allBotsStarted ? : null} - {allBotsStarted ? ( - - ) : ( - - )} - - void, trackedProjectIds: string[]) { + const savedCallback: any = useRef(); + const areBotsStarting = useRecoilValue(trackBotStatusesSelector(trackedProjectIds)); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = postSkillsStartAction; + }, [postSkillsStartAction]); + + useEffect(() => { + const trackedBotsStarted = !areBotsStarting; + if (trackedProjectIds.length && trackedBotsStarted) { + // Start the root bot now after skills are started. + savedCallback.current(); + } + }, [areBotsStarting]); +} diff --git a/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts new file mode 100644 index 0000000000..7851259797 --- /dev/null +++ b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { IPublishConfig } from '@bfc/shared'; +import { css } from '@emotion/core'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { botRuntimeOperationsSelector, buildConfigurationSelector } from '../../recoilModel'; + +import { useBotStatusTracker } from './useBotStatusTracker'; + +export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) { + const builderEssentials = useRecoilValue(buildConfigurationSelector); + const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); + const [trackedProjectIds, setProjectsToTrack] = useState([]); + + const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { + if (botBuildRequired) { + // Default recognizer + await botRuntimeOperations?.buildWithDefaultRecognizer(projectId, config); + } else { + // Regex recognizer + await botRuntimeOperations?.startBot(projectId, config); + } + }; + + const startRootBot = () => { + setProjectsToTrack([]); + const rootBot = builderEssentials[0]; + const { projectId, configuration, buildRequired } = rootBot; + handleBotStart(projectId, configuration, buildRequired); + }; + + // Custom hook to make sure root bot is started after all skills have been started. + useBotStatusTracker(() => { + startRootBot(); + }, trackedProjectIds); + + const startAllBots = async () => { + const [rootBot, ...skillsBots] = builderEssentials; + const trackProjects: string[] = skillsBots.map((skillBot) => skillBot.projectId); + setProjectsToTrack(trackProjects); + for (const botBuildConfig of skillsBots) { + const { projectId, configuration, buildRequired } = botBuildConfig; + await handleBotStart(projectId, configuration, buildRequired); + } + if (onAllBotsStarted) { + onAllBotsStarted(true); + } + }; + + const stopAllBots = () => { + setProjectsToTrack([]); + builderEssentials.forEach(({ projectId }) => botRuntimeOperations?.stopBot(projectId)); + if (onAllBotsStarted) { + onAllBotsStarted(false); + } + }; + + const startSingleBot = (projectId: string) => { + const skillData = builderEssentials.find((builder) => builder.projectId === projectId); + if (skillData) { + handleBotStart(projectId, skillData?.configuration, skillData?.buildRequired); + } + }; + + return { + stopAllBots, + startAllBots, + startSingleBot, + }; +} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index ceb9c265cd..1ff1009fe7 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -2,9 +2,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpaceSkill, convertAbsolutePathToFileProtocol, Skill } from '@bfc/shared'; +import { BotProjectSpaceSkill, convertPathToFileProtocol, Skill } from '@bfc/shared'; import { botNameIdentifierState, botProjectFileState, locationState, settingsState } from '../atoms'; import { rootBotProjectIdSelector } from '../selectors'; @@ -16,13 +18,15 @@ export const botProjectFileDispatcher = () => { if (!rootBotProjectId) { return; } + const rootBotLocation = await snapshot.getPromise(locationState(rootBotProjectId)); const skillLocation = await snapshot.getPromise(locationState(skillId)); const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { + const relativePath = path.relative(rootBotLocation, skillLocation); const skill: BotProjectSpaceSkill = { - workspace: convertAbsolutePathToFileProtocol(skillLocation), + workspace: convertPathToFileProtocol(relativePath), remote: false, }; draftState.content.skills[botName] = skill; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 9930bcce00..cf3babadb8 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -8,9 +8,11 @@ import { BotProjectSpaceSkill, convertFileProtocolToPath, convertSkillsToDictionary, + getEndpointNameGivenUrl, dereferenceDefinitions, DialogInfo, DialogSetting, + getManifestNameFromUrl, LuFile, QnAFile, SensitiveProperties, @@ -61,6 +63,7 @@ import { skillManifestsState, } from '../../atoms'; import * as botstates from '../../atoms/botState'; +import { dispatcherState } from '../../DispatcherWrapper'; import lgWorker from '../../parsers/lgWorker'; import luWorker from '../../parsers/luWorker'; import qnaWorker from '../../parsers/qnaWorker'; @@ -152,7 +155,7 @@ export const navigateToBot = ( } }; -const loadProjectData = (response) => { +export const loadProjectData = (response) => { const { files, botName, settings, id: projectId } = response.data; const mergedSettings = getMergedSettings(projectId, settings); const storedLocale = languageStorage.get(botName)?.locale; diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index 91b7f443ed..0d6b7bf9d4 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -6,4 +6,4 @@ export * from './eject'; export * from './extensions'; export * from './validatedDialogs'; export * from './skills'; -export * from './botRuntimeOperations'; +export * from './localRuntimeBuilder'; diff --git a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts similarity index 98% rename from Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts rename to Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts index 50d6942eee..dcf9ba0618 100644 --- a/Composer/packages/client/src/recoilModel/selectors/botRuntimeOperations.ts +++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts @@ -8,13 +8,14 @@ import settingsStorage from '../../utils/dialogSettingStorage'; import { BotStatus } from '../../constants'; import { isAbsHosted } from '../../utils/envUtil'; import { botStatusState, luFilesState, qnaFilesState, settingsState } from '../atoms'; -import { localBotsWithoutErrorsSelector } from '../selectors'; import { Dispatcher } from '../dispatchers'; import { dispatcherState } from '../DispatcherWrapper'; import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil'; import { validateDialogSelectorFamily } from './validatedDialogs'; +import { localBotsWithoutErrorsSelector } from '.'; + export const trackBotStatusesSelector = selectorFamily({ key: 'trackBotStatusesSelector', get: (trackedProjectIds: string[]) => ({ get }) => { diff --git a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts index 124c0e62cf..1a7a9316bc 100644 --- a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts +++ b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { convertAbsolutePathToFileProtocol, convertFileProtocolToPath } from '../../src/fileUtils'; +import { convertPathToFileProtocol, convertFileProtocolToPath } from '../../src/fileUtils'; it('should convert a posix path to file protocol', () => { const testPath = '/Users/tester/empty-bot-0'; - expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///Users/tester/empty-bot-0'); + expect(convertPathToFileProtocol(testPath)).toBe('file:///Users/tester/empty-bot-0'); }); it('should convert a windows path to file protocol', () => { const testPath = 'C:/Users/Tester/empty-bot-0'; - expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///C:/Users/Tester/empty-bot-0'); + expect(convertPathToFileProtocol(testPath)).toBe('file:///C:/Users/Tester/empty-bot-0'); }); it('should convert a Windows file protocol path to regular path', () => { diff --git a/Composer/plugins/localPublish/package.json b/Composer/plugins/localPublish/package.json index fb4ae25996..f4db2260bb 100644 --- a/Composer/plugins/localPublish/package.json +++ b/Composer/plugins/localPublish/package.json @@ -12,7 +12,9 @@ "dependencies": { "adm-zip": "^0.4.14", "archiver": "^5.0.2", + "get-port": "^5.1.1", "globby": "^11.0.0", + "kill-port": "^1.6.1", "path": "^0.12.7", "portfinder": "^1.0.26", "rimraf": "^3.0.2", diff --git a/Composer/plugins/localPublish/yarn.lock b/Composer/plugins/localPublish/yarn.lock index 6560c875a7..c167c4d02c 100644 --- a/Composer/plugins/localPublish/yarn.lock +++ b/Composer/plugins/localPublish/yarn.lock @@ -213,6 +213,16 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-them-args@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/get-them-args/-/get-them-args-1.3.2.tgz#74a20ba8a4abece5ae199ad03f2bcc68fdfc9ba5" + integrity sha1-dKILqKSr7OWuGZrQPyvMaP38m6U= + glob-parent@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" @@ -299,6 +309,14 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +kill-port@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/kill-port/-/kill-port-1.6.1.tgz#560fe79484583bdf3a5e908557dae614447618aa" + integrity sha512-un0Y55cOM7JKGaLnGja28T38tDDop0AQ8N0KlAdyh+B1nmMoX8AnNmqPNZbS3mUMgiST51DCVqmbFT1gNJpVNw== + dependencies: + get-them-args "1.3.2" + shell-exec "1.0.2" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -483,6 +501,11 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +shell-exec@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756" + integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" From 90be1721ff3ffb1f08025224b5371a3202403337 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 16 Oct 2020 01:30:46 -0700 Subject: [PATCH 038/100] relative path support bp file Signed-off-by: Srinaath Ravichandran Path set Signed-off-by: Srinaath Ravichandran --- .../TestController/startBotsPanel.tsx | 4 +-- Composer/packages/client/src/constants.ts | 4 +-- .../client/src/pages/design/DesignPage.tsx | 30 ++++++++++++++++ .../recoilModel/dispatchers/botProjectFile.ts | 5 ++- .../src/recoilModel/dispatchers/project.ts | 4 +-- .../recoilModel/dispatchers/utils/project.ts | 21 ++++++----- .../selectors/localRuntimeBuilder.ts | 3 +- .../src/recoilModel/selectors/project.ts | 17 +++++++-- .../shared/__tests__/fileUtils/index.test.ts | 28 --------------- .../lib/shared/src/fileUtils/index.ts | 22 ------------ Composer/packages/lib/shared/src/index.ts | 1 - .../server/src/models/bot/botProject.ts | 14 ++------ Composer/packages/types/src/indexers.ts | 1 - Composer/plugins/localPublish/src/index.ts | 35 +++++++++++-------- 14 files changed, 89 insertions(+), 100 deletions(-) delete mode 100644 Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts delete mode 100644 Composer/packages/lib/shared/src/fileUtils/index.ts diff --git a/Composer/packages/client/src/components/TestController/startBotsPanel.tsx b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx index 25eca94728..1bb7a31711 100644 --- a/Composer/packages/client/src/components/TestController/startBotsPanel.tsx +++ b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx @@ -19,7 +19,7 @@ import { import { useRecoilValue } from 'recoil'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; -import { botProjectSpaceSelector } from '../../recoilModel'; +import { botProjectSpaceSelector, localBotsDataSelector } from '../../recoilModel'; import { LocalBotStatusIndicator } from './LocalBotStatusIndicator'; import { LocalBotRuntime } from './LocalBotRuntime'; @@ -58,7 +58,7 @@ interface StartBotsPanelProps { export const StartBotsPanel: React.FC = (props) => { const { isOpen, onDismiss } = props; - const projectCollection = useRecoilValue(botProjectSpaceSelector); + const projectCollection = useRecoilValue(localBotsDataSelector); const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); useEffect(() => { diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index bfa0455238..221cef5a1f 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -99,11 +99,11 @@ export const Steps = { export const BotStatusesCopy = { connected: formatMessage('Running'), - pubishing: formatMessage('Building'), + publishing: formatMessage('Publishing'), published: formatMessage('Starting'), unConnected: formatMessage('Inactive'), failed: formatMessage('Failed to start'), - reloading: formatMessage('Reloading'), + loading: formatMessage('Loading'), }; export const DialogCreationCopy = { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 2afa8b5ed1..e97024347a 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -122,6 +122,7 @@ const DesignPage: React.FC { + addExistingSkillToBotProject('/Users/srravich/Desktop/EchoBot-new'); + }, + }, + { + 'data-testid': 'createNewSkill', + key: 'createNewSkill', + text: formatMessage(`Create new Skill`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + addNewSkillToBotProject({ + name: 'newers-bot', + description: '', + schemaUrl: '', + location: '/Users/srravich/Desktop/samples', + templateId: 'InterruptionSample', + locale: appLocale, + qnaKbUrls: [], + }); + }, + }, { 'data-testid': 'AddLocalSkill', key: 'addLocalSkill', diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index 1ff1009fe7..c51b9c8cbe 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -6,7 +6,7 @@ import path from 'path'; import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpaceSkill, convertPathToFileProtocol, Skill } from '@bfc/shared'; +import { BotProjectSpaceSkill, Skill } from '@bfc/shared'; import { botNameIdentifierState, botProjectFileState, locationState, settingsState } from '../atoms'; import { rootBotProjectIdSelector } from '../selectors'; @@ -26,7 +26,7 @@ export const botProjectFileDispatcher = () => { const result = produce(current, (draftState) => { const relativePath = path.relative(rootBotLocation, skillLocation); const skill: BotProjectSpaceSkill = { - workspace: convertPathToFileProtocol(relativePath), + workspace: relativePath, remote: false, }; draftState.content.skills[botName] = skill; @@ -42,7 +42,6 @@ export const botProjectFileDispatcher = () => { return; } const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); - set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { const skill: BotProjectSpaceSkill = { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 2ea941b04c..26433f3bbd 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -124,9 +124,9 @@ export const projectDispatcher = () => { formatMessage('This operation cannot be completed. The skill is already part of the Bot Project') ); } - const skillNameIdentifier: string = await getSkillNameIdentifier(callbackHelpers, name); + set(botOpeningState, true); - const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl, skillNameIdentifier); + const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl); set(botProjectIdsState, (current) => [...current, projectId]); await dispatcher.addRemoteSkillToBotProjectFile(projectId, manifestUrl, endpointName); } catch (ex) { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index cf3babadb8..752957f452 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { indexer, validateDialog } from '@bfc/indexers'; import { BotProjectFile, BotProjectSpace, BotProjectSpaceSkill, - convertFileProtocolToPath, convertSkillsToDictionary, getEndpointNameGivenUrl, dereferenceDefinitions, @@ -342,7 +343,7 @@ export const removeRecentProject = async (callbackHelpers: CallbackInterface, pa export const openRemoteSkill = async ( callbackHelpers: CallbackInterface, manifestUrl: string, - botNameIdentifier: string + botNameIdentifier?: string ) => { const { set } = callbackHelpers; @@ -358,7 +359,8 @@ export const openRemoteSkill = async ( isRootBot: false, isRemote: true, }); - set(botNameIdentifierState(projectId), botNameIdentifier); + + set(botNameIdentifierState(projectId), botNameIdentifier || camelCase(manifestResponse.data.name)); set(botDisplayNameState(projectId), manifestResponse.data.name); set(locationState(projectId), manifestUrl); set(skillManifestsState(projectId), [ @@ -478,7 +480,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); const rootBotProjectId = projectData.id; - const { name } = projectData; + const { name, location } = projectData; const { mergedSettings } = botFiles; set(botNameIdentifierState(rootBotProjectId), camelCase(name)); @@ -500,8 +502,10 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const skill = skills[nameIdentifier]; let skillPromise; if (!skill.remote && skill.workspace) { - const skillPath = convertFileProtocolToPath(skill.workspace); - skillPromise = openLocalSkill(callbackHelpers, skillPath, storageId, nameIdentifier); + const rootBotPath = location; + const skillPath = skill.workspace; + const absoluteSkillPath = path.resolve(rootBotPath, skillPath); + skillPromise = openLocalSkill(callbackHelpers, absoluteSkillPath, storageId, nameIdentifier); } else if (skill.manifest) { skillPromise = openRemoteSkill(callbackHelpers, skill.manifest, nameIdentifier); } @@ -591,6 +595,7 @@ export const checkIfBotExistsInBotProjectFile = async ( if (!rootBotProjectId) { throw new Error(formatMessage('The root bot is not a bot project')); } + const rootBotLocation = await snapshot.getPromise(locationState(rootBotProjectId)); const { content: botProjectFile } = await snapshot.getPromise(botProjectFileState(rootBotProjectId)); for (const uniqueSkillName in botProjectFile.skills) { @@ -601,8 +606,8 @@ export const checkIfBotExistsInBotProjectFile = async ( } } else { if (workspace) { - const resolvedPath = convertFileProtocolToPath(workspace); - if (pathOrManifest === resolvedPath) { + const absolutePathOfSkill = path.resolve(rootBotLocation, workspace); + if (pathOrManifest === absolutePathOfSkill) { return true; } } diff --git a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts index dcf9ba0618..6648ff714f 100644 --- a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts +++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts @@ -13,8 +13,7 @@ import { dispatcherState } from '../DispatcherWrapper'; import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil'; import { validateDialogSelectorFamily } from './validatedDialogs'; - -import { localBotsWithoutErrorsSelector } from '.'; +import { localBotsWithoutErrorsSelector } from './project'; export const trackBotStatusesSelector = selectorFamily({ key: 'trackBotStatusesSelector', diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index ce7ff96cd1..f4c5b2c973 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -25,12 +25,25 @@ export const localBotsWithoutErrorsSelector = selector({ return botProjectIds.filter((projectId: string) => { const { isRemote } = get(projectMetaDataState(projectId)); const botError = get(botErrorState(projectId)); - return !botError && !isRemote; + return !isRemote && !botError; }); }, }); -// TODO: This selector would be modfied and leveraged by the project tree +export const localBotsDataSelector = selector({ + key: 'localBotsDataSelector', + get: ({ get }) => { + const botProjectIds = get(localBotsWithoutErrorsSelector); + return botProjectIds.map((projectId: string) => { + return { + projectId, + name: get(botDisplayNameState(projectId)), + }; + }); + }, +}); + +// TODO: Selector used in Design Page view with remote bot data export const botProjectSpaceSelector = selector({ key: 'botProjectSpaceSelector', get: ({ get }) => { diff --git a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts deleted file mode 100644 index 1a7a9316bc..0000000000 --- a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { convertPathToFileProtocol, convertFileProtocolToPath } from '../../src/fileUtils'; - -it('should convert a posix path to file protocol', () => { - const testPath = '/Users/tester/empty-bot-0'; - expect(convertPathToFileProtocol(testPath)).toBe('file:///Users/tester/empty-bot-0'); -}); - -it('should convert a windows path to file protocol', () => { - const testPath = 'C:/Users/Tester/empty-bot-0'; - expect(convertPathToFileProtocol(testPath)).toBe('file:///C:/Users/Tester/empty-bot-0'); -}); - -it('should convert a Windows file protocol path to regular path', () => { - const testPath = 'file:///C:/Users/Tester/empty-bot-0'; - expect(convertFileProtocolToPath(testPath)).toBe('C:/Users/Tester/empty-bot-0'); -}); - -it('should convert a Mac file protocol path to regular path', () => { - const testPath = 'file:///users/tester/empty-bot-0'; - expect(convertFileProtocolToPath(testPath)).toBe('/users/tester/empty-bot-0'); -}); - -it('should give empty string if path is not available', () => { - const testPath = ''; - expect(convertFileProtocolToPath(testPath)).toBe(''); -}); diff --git a/Composer/packages/lib/shared/src/fileUtils/index.ts b/Composer/packages/lib/shared/src/fileUtils/index.ts deleted file mode 100644 index 0cb4cf7829..0000000000 --- a/Composer/packages/lib/shared/src/fileUtils/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import trimStart from 'lodash/trimStart'; - -export const convertFileProtocolToPath = (pathToBot: string): string => { - const fileProtocolRemoved = pathToBot.replace('file://', ''); - if (fileProtocolRemoved.match(/^\/[a-zA-Z]:\//g)) { - //Windows path with file protocol. Remove leading / - return trimStart(fileProtocolRemoved, '/'); - } - return fileProtocolRemoved; -}; - -export const convertAbsolutePathToFileProtocol = (pathToBot: string): string => { - let pathName = pathToBot.replace(/\\/g, '/'); - // Windows drive letter must be prefixed with a slash - if (pathName[0] !== '/') { - pathName = '/' + pathName; - } - return encodeURI('file://' + pathName); -}; diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index f21fcd1e18..a5d72c06cf 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -25,5 +25,4 @@ export * from './schemaUtils'; export * from './viewUtils'; export * from './walkerUtils'; export * from './skillsUtils'; -export * from './fileUtils'; export const DialogUtils = dialogUtils; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index ca2dba5ce2..d12494cb4c 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -6,15 +6,7 @@ import fs from 'fs'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; -import { - getNewDesigner, - FileInfo, - Diagnostic, - IBotProject, - DialogSetting, - FileExtensions, - convertAbsolutePathToFileProtocol, -} from '@bfc/shared'; +import { getNewDesigner, FileInfo, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; import merge from 'lodash/merge'; import { UserIdentity, ExtensionContext } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; @@ -365,7 +357,7 @@ export class BotProject implements IBotProject { for (const botProjectFile of this.botProjectFiles) { const { relativePath } = botProjectFile; const content = JSON.parse(botProjectFile.content); - content.workspace = convertAbsolutePathToFileProtocol(this.dataDir); + content.workspace = this.dataDir; content.name = botName; await this._updateFile(relativePath, JSON.stringify(content, null, 2)); } @@ -806,8 +798,6 @@ export class BotProject implements IBotProject { } const fileName = `${this.name}${FileExtensions.BotProject}`; const root = this.dataDir; - - defaultBotProjectFile.workspace = convertAbsolutePathToFileProtocol(root); defaultBotProjectFile.name = this.name; await this._createFile(fileName, JSON.stringify(defaultBotProjectFile, null, 2)); diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index 9e09c8e125..0c22c00ff4 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -206,7 +206,6 @@ export interface BotProjectSpaceSkill { } export interface BotProjectSpace { - workspace: string; name: string; skills: { [skillId: string]: BotProjectSpaceSkill; diff --git a/Composer/plugins/localPublish/src/index.ts b/Composer/plugins/localPublish/src/index.ts index cefb36dcd2..a852718355 100644 --- a/Composer/plugins/localPublish/src/index.ts +++ b/Composer/plugins/localPublish/src/index.ts @@ -11,6 +11,10 @@ import archiver from 'archiver'; import { v4 as uuid } from 'uuid'; import AdmZip from 'adm-zip'; import portfinder from 'portfinder'; +import killPort from 'kill-port'; +import map from 'lodash/map'; +import range from 'lodash/range'; +import getPort from 'get-port'; const stat = promisify(fs.stat); const readDir = promisify(fs.readdir); @@ -260,6 +264,13 @@ class LocalPublisher { await this.zipBot(dstPath, srcDir); }; + private getAvailablePorts = (): number[] => { + const excludePorts = map(LocalPublisher.runningBots, 'port'); + const portRanges = range(3979, 5000); + const filtered = portRanges.filter((current) => !excludePorts.includes(current)); + return filtered; + }; + // start bot in current version private setBot = async (botId: string, version: string, settings: any, project: any) => { // get port, and stop previous bot if exist @@ -272,7 +283,8 @@ class LocalPublisher { this.stopBot(botId); } if (!port) { - port = await portfinder.getPortPromise({ port: 3979, stopPort: 5000 }); + port = await getPort({ port: this.getAvailablePorts() }); + // port = await portfinder.getPortPromise({ port: 3979, stopPort: 5000 }); } // if not using custom runtime, update assets in tmp older @@ -446,22 +458,15 @@ class LocalPublisher { }; // make it public, so that able to stop runtime before switch ejected runtime. - public stopBot = (botId: string) => { + public stopBot = async (botId: string) => { const proc = LocalPublisher.runningBots[botId]?.process; + const port = LocalPublisher.runningBots[botId]?.port; - if (proc) { - this.composer.log('Killing process %d', -proc.pid); - // Kill the bot process AND all child processes - try { - this.removeListener(proc); - process.kill(isWin ? proc.pid : -proc.pid); - } catch (err) { - // ESRCH means pid not found - // this throws an error but doesn't indicate failure for us - if (err.code !== 'ESRCH') { - throw err; - } - } + if (port) { + this.composer.log('Killing process at port %d', port); + + this.removeListener(proc); + await killPort(port); } delete LocalPublisher.runningBots[botId]; }; From 0a688c43e6a193954b4f9c58aaa770aa8514d1e5 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 16 Oct 2020 01:35:50 -0700 Subject: [PATCH 039/100] Fix schema Signed-off-by: Srinaath Ravichandran --- Composer/packages/server/schemas/botproject.schema | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema index 304d559cb6..fe45b61f18 100644 --- a/Composer/packages/server/schemas/botproject.schema +++ b/Composer/packages/server/schemas/botproject.schema @@ -16,10 +16,6 @@ "type": "string", "description": "Name of the Root Bot." }, - "workspace": { - "type": "string", - "description": "Absolute path to the Root Bot. If a workspace is local, we use the file protocol as opposed to http/https for remote workspaces." - }, "skills": { "description": "List of skills (remote or local) skills that the Root Bot consumes.", "type": "object", @@ -54,7 +50,7 @@ }, "workspace": { "type": "string", - "description": "Absolute path to a skill workspace. If a workspace is local, we use the file protocol as opposed to http/https protocols for remote workspaces." + "description": "Path to the skill always relative to the Root Bot." }, "remote": { "description": "Indication if the skill is remote or local skill.", From 1f8a9c4b54b64b96d1737062401c5a4c1dadd14b Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 16 Oct 2020 01:40:44 -0700 Subject: [PATCH 040/100] Removed workspace references. Signed-off-by: Srinaath Ravichandran --- .../samples/assets/projects/ActionsSample/actionssample.botproj | 1 - .../projects/AskingQuestionsSample/askingquestionssample.botproj | 1 - .../controllingconversationflowsample.botproj | 1 - Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj | 1 - .../plugins/samples/assets/projects/EmptyBot/emptybot.botproj | 1 - .../projects/InterruptionSample/interruptionssample.botproj | 1 - .../projects/QnAMakerLUISSample/qnamakerluissample.botproj | 1 - .../plugins/samples/assets/projects/QnASample/qnasample.botproj | 1 - .../RespondingWithCardsSample/respondingwithcardssample.botproj | 1 - .../RespondingWithTextSample/respondingwithtextsample.botproj | 1 - .../projects/ToDoBotWithLuisSample/todobotwithluissample.botproj | 1 - .../samples/assets/projects/TodoSample/todosample.botproj | 1 - 12 files changed, 12 deletions(-) diff --git a/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj +++ b/Composer/plugins/samples/assets/projects/ActionsSample/actionssample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj +++ b/Composer/plugins/samples/assets/projects/AskingQuestionsSample/askingquestionssample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj +++ b/Composer/plugins/samples/assets/projects/ControllingConversationFlowSample/controllingconversationflowsample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj +++ b/Composer/plugins/samples/assets/projects/EchoBot/echobot.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj +++ b/Composer/plugins/samples/assets/projects/EmptyBot/emptybot.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj +++ b/Composer/plugins/samples/assets/projects/InterruptionSample/interruptionssample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj +++ b/Composer/plugins/samples/assets/projects/QnAMakerLUISSample/qnamakerluissample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj +++ b/Composer/plugins/samples/assets/projects/QnASample/qnasample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj +++ b/Composer/plugins/samples/assets/projects/RespondingWithCardsSample/respondingwithcardssample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj +++ b/Composer/plugins/samples/assets/projects/RespondingWithTextSample/respondingwithtextsample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj +++ b/Composer/plugins/samples/assets/projects/ToDoBotWithLuisSample/todobotwithluissample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } diff --git a/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj index 543cc3b976..daf6d1c8fe 100644 --- a/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj +++ b/Composer/plugins/samples/assets/projects/TodoSample/todosample.botproj @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/main/Composer/packages/server/schemas/botproject.schema", "name": "", - "workspace": "", "skills": {} } From 6970cc59799b6a968b9b52ce137802f1730d7329 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 16 Oct 2020 01:50:49 -0700 Subject: [PATCH 041/100] Relative path support Signed-off-by: Srinaath Ravichandran --- .../src/recoilModel/dispatchers/project.ts | 4 ++-- .../recoilModel/dispatchers/utils/project.ts | 22 ++++++++++++------- .../lib/shared/src/fileUtils/index.ts | 22 ------------------- Composer/packages/lib/shared/src/index.ts | 1 - .../server/src/models/bot/botProject.ts | 15 ++----------- Composer/packages/types/src/indexers.ts | 1 - 6 files changed, 18 insertions(+), 47 deletions(-) delete mode 100644 Composer/packages/lib/shared/src/fileUtils/index.ts diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 32c1e4f51d..2f54389453 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -124,9 +124,9 @@ export const projectDispatcher = () => { formatMessage('This operation cannot be completed. The skill is already part of the Bot Project') ); } - const skillNameIdentifier: string = await getSkillNameIdentifier(callbackHelpers, name); + set(botOpeningState, true); - const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl, skillNameIdentifier); + const { projectId } = await openRemoteSkill(callbackHelpers, manifestUrl); set(botProjectIdsState, (current) => [...current, projectId]); await dispatcher.addRemoteSkillToBotProjectFile(projectId, manifestUrl, endpointName); } catch (ex) { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index e0bffa6dd2..01b112384b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { indexer, validateDialog } from '@bfc/indexers'; import { BotProjectFile, BotProjectSpace, BotProjectSpaceSkill, - convertFileProtocolToPath, convertSkillsToDictionary, dereferenceDefinitions, DialogInfo, @@ -350,7 +351,7 @@ export const removeRecentProject = async (callbackHelpers: CallbackInterface, pa export const openRemoteSkill = async ( callbackHelpers: CallbackInterface, manifestUrl: string, - botNameIdentifier: string + botNameIdentifier?: string ) => { const { set } = callbackHelpers; @@ -366,7 +367,8 @@ export const openRemoteSkill = async ( isRootBot: false, isRemote: true, }); - set(botNameIdentifierState(projectId), botNameIdentifier); + + set(botNameIdentifierState(projectId), botNameIdentifier || camelCase(manifestResponse.data.name)); set(botDisplayNameState(projectId), manifestResponse.data.name); set(locationState(projectId), manifestUrl); return { projectId, manifestResponse: manifestResponse.data }; @@ -458,7 +460,8 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); const rootBotProjectId = projectData.id; - const { name } = projectData; + const { name, location } = projectData; + set(botNameIdentifierState(rootBotProjectId), camelCase(name)); if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { @@ -477,8 +480,10 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st const skill = skills[nameIdentifier]; let skillPromise; if (!skill.remote && skill.workspace) { - const skillPath = convertFileProtocolToPath(skill.workspace); - skillPromise = openLocalSkill(callbackHelpers, skillPath, storageId, nameIdentifier); + const rootBotPath = location; + const skillPath = skill.workspace; + const absoluteSkillPath = path.resolve(rootBotPath, skillPath); + skillPromise = openLocalSkill(callbackHelpers, absoluteSkillPath, storageId, nameIdentifier); } else if (skill.manifest) { skillPromise = openRemoteSkill(callbackHelpers, skill.manifest, nameIdentifier); } @@ -564,6 +569,7 @@ export const checkIfBotExistsInBotProjectFile = async ( if (!rootBotProjectId) { throw new Error(formatMessage('The root bot is not a bot project')); } + const rootBotLocation = await snapshot.getPromise(locationState(rootBotProjectId)); const { content: botProjectFile } = await snapshot.getPromise(botProjectFileState(rootBotProjectId)); for (const uniqueSkillName in botProjectFile.skills) { @@ -574,8 +580,8 @@ export const checkIfBotExistsInBotProjectFile = async ( } } else { if (workspace) { - const resolvedPath = convertFileProtocolToPath(workspace); - if (pathOrManifest === resolvedPath) { + const absolutePathOfSkill = path.resolve(rootBotLocation, workspace); + if (pathOrManifest === absolutePathOfSkill) { return true; } } diff --git a/Composer/packages/lib/shared/src/fileUtils/index.ts b/Composer/packages/lib/shared/src/fileUtils/index.ts deleted file mode 100644 index 0cb4cf7829..0000000000 --- a/Composer/packages/lib/shared/src/fileUtils/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import trimStart from 'lodash/trimStart'; - -export const convertFileProtocolToPath = (pathToBot: string): string => { - const fileProtocolRemoved = pathToBot.replace('file://', ''); - if (fileProtocolRemoved.match(/^\/[a-zA-Z]:\//g)) { - //Windows path with file protocol. Remove leading / - return trimStart(fileProtocolRemoved, '/'); - } - return fileProtocolRemoved; -}; - -export const convertAbsolutePathToFileProtocol = (pathToBot: string): string => { - let pathName = pathToBot.replace(/\\/g, '/'); - // Windows drive letter must be prefixed with a slash - if (pathName[0] !== '/') { - pathName = '/' + pathName; - } - return encodeURI('file://' + pathName); -}; diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index 2d5f44518f..57d7843a58 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -25,5 +25,4 @@ export * from './schemaUtils'; export * from './viewUtils'; export * from './walkerUtils'; export * from './skillsUtils'; -export * from './fileUtils'; export const DialogUtils = dialogUtils; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 72f21cefd9..9f3383ff39 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -6,16 +6,7 @@ import fs from 'fs'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; -import { - getNewDesigner, - FileInfo, - Skill, - Diagnostic, - IBotProject, - DialogSetting, - FileExtensions, - convertAbsolutePathToFileProtocol, -} from '@bfc/shared'; +import { getNewDesigner, FileInfo, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; import merge from 'lodash/merge'; import { UserIdentity, ExtensionContext } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; @@ -372,7 +363,7 @@ export class BotProject implements IBotProject { for (const botProjectFile of this.botProjectFiles) { const { relativePath } = botProjectFile; const content = JSON.parse(botProjectFile.content); - content.workspace = convertAbsolutePathToFileProtocol(this.dataDir); + content.workspace = this.dataDir; content.name = botName; await this._updateFile(relativePath, JSON.stringify(content, null, 2)); } @@ -814,8 +805,6 @@ export class BotProject implements IBotProject { } const fileName = `${this.name}${FileExtensions.BotProject}`; const root = this.dataDir; - - defaultBotProjectFile.workspace = convertAbsolutePathToFileProtocol(root); defaultBotProjectFile.name = this.name; await this._createFile(fileName, JSON.stringify(defaultBotProjectFile, null, 2)); diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index c0293bce42..1625e750ba 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -225,7 +225,6 @@ export interface BotProjectSpaceSkill { } export interface BotProjectSpace { - workspace: string; name: string; skills: { [skillId: string]: BotProjectSpaceSkill; From 84f0a507961f4934763c12c2e0475b90a51d2d91 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Fri, 16 Oct 2020 16:08:05 -0700 Subject: [PATCH 042/100] Update operations on Bot project file Signed-off-by: Srinaath Ravichandran --- .../recoilModel/dispatchers/botProjectFile.ts | 82 ++++++++++++------- .../server/src/models/bot/botProject.ts | 2 +- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index ad523b1498..c1104cf171 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -2,44 +2,45 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { CallbackInterface, useRecoilCallback } from 'recoil'; import { produce } from 'immer'; -import { BotProjectSpaceSkill, convertAbsolutePathToFileProtocol } from '@bfc/shared'; +import { BotProjectSpaceSkill } from '@bfc/shared'; import { botNameIdentifierState, botProjectFileState, locationState } from '../atoms'; import { rootBotProjectIdSelector } from '../selectors'; export const botProjectFileDispatcher = () => { - const addLocalSkillToBotProjectFile = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { - const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); - if (!rootBotProjectId) { - return; - } - const skillLocation = await snapshot.getPromise(locationState(skillId)); - const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + const addLocalSkill = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + return; + } + const rootBotLocation = await snapshot.getPromise(locationState(rootBotProjectId)); + const skillLocation = await snapshot.getPromise(locationState(skillId)); + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); - set(botProjectFileState(rootBotProjectId), (current) => { - const result = produce(current, (draftState) => { - const skill: BotProjectSpaceSkill = { - workspace: convertAbsolutePathToFileProtocol(skillLocation), - remote: false, - }; - draftState.content.skills[botName] = skill; - }); - return result; + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + const relativePath = path.relative(rootBotLocation, skillLocation); + const skill: BotProjectSpaceSkill = { + workspace: relativePath, + remote: false, + }; + draftState.content.skills[botName] = skill; }); - } - ); + return result; + }); + }); - const addRemoteSkillToBotProjectFile = useRecoilCallback( + const addRemoteSkill = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async (skillId: string, manifestUrl: string, endpointName: string) => { const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; } const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); - set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { const skill: BotProjectSpaceSkill = { @@ -55,17 +56,39 @@ export const botProjectFileDispatcher = () => { } ); - const removeSkillFromBotProjectFile = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const removeSkill = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async (skillId: string) => { + const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotProjectId) { + return; + } + + const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); + set(botProjectFileState(rootBotProjectId), (current) => { + const result = produce(current, (draftState) => { + delete draftState.content.skills[botName]; + }); + return result; + }); + }); + + const updateManifest = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (skillProjectId: string, manifestId: string | undefined) => { const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; } + const skillNameIdentifier = await snapshot.getPromise(botNameIdentifierState(skillProjectId)); - const botName = await snapshot.getPromise(botNameIdentifierState(skillId)); set(botProjectFileState(rootBotProjectId), (current) => { const result = produce(current, (draftState) => { - delete draftState.content.skills[botName]; + if (!manifestId) { + delete draftState[skillNameIdentifier].manifest; + } else { + draftState[skillNameIdentifier] = { + ...draftState[skillNameIdentifier], + manifest: manifestId, + }; + } }); return result; }); @@ -73,8 +96,9 @@ export const botProjectFileDispatcher = () => { ); return { - addLocalSkillToBotProjectFile, - removeSkillFromBotProjectFile, - addRemoteSkillToBotProjectFile, + addLocalSkillToBotProjectFile: addLocalSkill, + removeSkillFromBotProjectFile: removeSkill, + addRemoteSkillToBotProjectFile: addRemoteSkill, + updateManifestInBotProjectFile: updateManifest, }; }; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 9f3383ff39..da1b18da96 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; -import { getNewDesigner, FileInfo, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; +import { getNewDesigner, FileInfo, Diagnostic, IBotProject, DialogSetting, FileExtensions, Skill } from '@bfc/shared'; import merge from 'lodash/merge'; import { UserIdentity, ExtensionContext } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; From 96e3fb42b86bc38129b71ab5e90051940e282069 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Sun, 18 Oct 2020 20:57:05 -0700 Subject: [PATCH 043/100] Updated runtime code Signed-off-by: Srinaath Ravichandran --- .../TestController/LocalBotRuntime.tsx | 71 ++++++++++--------- .../LocalBotStatusIndicator.tsx | 1 - .../TestController/startBotsPanel.tsx | 4 +- .../TestController/useBotStatusTracker.ts | 10 +-- .../TestController/useLocalBotOperations.ts | 28 ++++++-- Composer/packages/client/src/constants.ts | 2 +- .../client/src/pages/design/DesignPage.tsx | 67 ++++++++++------- .../recoilModel/dispatchers/botProjectFile.ts | 3 +- .../src/recoilModel/dispatchers/setting.ts | 22 ++++++ .../src/recoilModel/dispatchers/skill.ts | 55 ++++++++++++-- .../recoilModel/dispatchers/utils/project.ts | 15 ++-- .../selectors/localRuntimeBuilder.ts | 8 ++- .../src/recoilModel/selectors/skills.ts | 1 - .../packages/client/src/shell/useShell.ts | 3 +- 14 files changed, 198 insertions(+), 92 deletions(-) diff --git a/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx index 0c30639192..41393a7001 100644 --- a/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx @@ -2,26 +2,34 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx } from '@emotion/core'; +import { jsx, css } from '@emotion/core'; import { useRecoilValue } from 'recoil'; import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; -import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { ActionButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; -import { IPublishConfig } from '@bfc/shared'; +import { Spinner } from 'office-ui-fabric-react/lib/Spinner'; +import { useCallback } from 'react'; import { botStatusState } from '../../recoilModel'; import { BotStatus } from '../../constants'; -import { - botBuildRequiredSelector, - botRuntimeOperationsSelector, - buildEssentialsSelector, -} from '../../recoilModel/selectors/localRuntimeBuilder'; + +import { useBotOperations } from './useLocalBotOperations'; interface LocalBotRuntimeProps { projectId: string; displayName: string; } +const localBotRuntimeContainerStyles = css` + display: flex; + align-items: center; + justify-content: center; +`; + +const loadingSpinnerStyle = css` + margin-right: 12px; +`; + const icon: IButtonStyles = { root: { color: `${SharedColors.cyanBlue20}`, @@ -34,36 +42,31 @@ const icon: IButtonStyles = { export const LocalBotRuntime: React.FC = ({ projectId, displayName }) => { const currentBotStatus = useRecoilValue(botStatusState(projectId)); - const botBuildRequired = useRecoilValue(botBuildRequiredSelector(projectId)); - const { configuration: startBotConfig } = useRecoilValue(buildEssentialsSelector(projectId)); - const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); - - const handleBotStop = () => { - botRuntimeOperations?.stopBot(projectId); - }; + const { startSingleBot, stopSingleBot } = useBotOperations(); - const handleBotStart = async () => { - const config: IPublishConfig | undefined = startBotConfig; - if (botBuildRequired) { - // Default recognizer - botRuntimeOperations?.buildWithDefaultRecognizer(projectId, config); - } else { - // Regex recognizer - botRuntimeOperations?.startBot(projectId, config); + const botRunIndicatorCallback = useCallback(() => { + switch (currentBotStatus) { + case BotStatus.connected: + case BotStatus.failed: + return ( + stopSingleBot(projectId)}> + + + ); + case BotStatus.unConnected: + return ( + startSingleBot(projectId)}> + + + ); + default: + return ; } - }; + }, [currentBotStatus]); return ( -
- {currentBotStatus === BotStatus.connected ? ( - - ) : ( - - )} +
+ {botRunIndicatorCallback()} {displayName}
); diff --git a/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx index f97a58352f..67d9724569 100644 --- a/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx @@ -19,7 +19,6 @@ import { ErrorCallout } from './errorCallout'; import { useBotOperations } from './useLocalBotOperations'; const actionButton = css` - font-size: 18px; color: #0078d4; `; diff --git a/Composer/packages/client/src/components/TestController/startBotsPanel.tsx b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx index 1bb7a31711..643498dfc4 100644 --- a/Composer/packages/client/src/components/TestController/startBotsPanel.tsx +++ b/Composer/packages/client/src/components/TestController/startBotsPanel.tsx @@ -19,7 +19,7 @@ import { import { useRecoilValue } from 'recoil'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; -import { botProjectSpaceSelector, localBotsDataSelector } from '../../recoilModel'; +import { buildConfigurationSelector } from '../../recoilModel'; import { LocalBotStatusIndicator } from './LocalBotStatusIndicator'; import { LocalBotRuntime } from './LocalBotRuntime'; @@ -58,7 +58,7 @@ interface StartBotsPanelProps { export const StartBotsPanel: React.FC = (props) => { const { isOpen, onDismiss } = props; - const projectCollection = useRecoilValue(localBotsDataSelector); + const projectCollection = useRecoilValue(buildConfigurationSelector); const [items, setItems] = useState<{ displayName: string; projectId: string }[]>([]); useEffect(() => { diff --git a/Composer/packages/client/src/components/TestController/useBotStatusTracker.ts b/Composer/packages/client/src/components/TestController/useBotStatusTracker.ts index 9bf066699f..636999bdc8 100644 --- a/Composer/packages/client/src/components/TestController/useBotStatusTracker.ts +++ b/Composer/packages/client/src/components/TestController/useBotStatusTracker.ts @@ -6,18 +6,18 @@ import { useRecoilValue } from 'recoil'; import { trackBotStatusesSelector } from '../../recoilModel'; -export function useBotStatusTracker(postSkillsStartAction: () => void, trackedProjectIds: string[]) { +export function useBotStatusTracker(postTrackedBotsStartedAction: () => void, trackedProjectIds: string[]) { const savedCallback: any = useRef(); const areBotsStarting = useRecoilValue(trackBotStatusesSelector(trackedProjectIds)); // Remember the latest callback. useEffect(() => { - savedCallback.current = postSkillsStartAction; - }, [postSkillsStartAction]); + savedCallback.current = postTrackedBotsStartedAction; + }, [postTrackedBotsStartedAction]); useEffect(() => { - const trackedBotsStarted = !areBotsStarting; - if (trackedProjectIds.length && trackedBotsStarted) { + const allTrackedBotStarted = !areBotsStarting; + if (trackedProjectIds.length && allTrackedBotStarted) { // Start the root bot now after skills are started. savedCallback.current(); } diff --git a/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts index 7851259797..2db0b98436 100644 --- a/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts +++ b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts @@ -2,18 +2,24 @@ // Licensed under the MIT License. import { IPublishConfig } from '@bfc/shared'; -import { css } from '@emotion/core'; import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { botRuntimeOperationsSelector, buildConfigurationSelector } from '../../recoilModel'; +import { + botRuntimeOperationsSelector, + buildConfigurationSelector, + dispatcherState, + rootBotProjectIdSelector, +} from '../../recoilModel'; import { useBotStatusTracker } from './useBotStatusTracker'; export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) { const builderEssentials = useRecoilValue(buildConfigurationSelector); const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); + const rootBotId = useRecoilValue(rootBotProjectIdSelector); const [trackedProjectIds, setProjectsToTrack] = useState([]); + const { updateSettingForLocalEndpointSkills } = useRecoilValue(dispatcherState); const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { if (botBuildRequired) { @@ -25,8 +31,9 @@ export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) } }; - const startRootBot = () => { + const startRootBot = async () => { setProjectsToTrack([]); + await updateSettingForLocalEndpointSkills(); const rootBot = builderEssentials[0]; const { projectId, configuration, buildRequired } = rootBot; handleBotStart(projectId, configuration, buildRequired); @@ -59,15 +66,24 @@ export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) }; const startSingleBot = (projectId: string) => { - const skillData = builderEssentials.find((builder) => builder.projectId === projectId); - if (skillData) { - handleBotStart(projectId, skillData?.configuration, skillData?.buildRequired); + if (projectId === rootBotId) { + startRootBot(); + } else { + const botData = builderEssentials.find((builder) => builder.projectId === projectId); + if (botData) { + handleBotStart(projectId, botData?.configuration, botData?.buildRequired); + } } }; + const stopSingleBot = (projectId: string) => { + botRuntimeOperations?.stopBot(projectId); + }; + return { stopAllBots, startAllBots, startSingleBot, + stopSingleBot, }; } diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index 221cef5a1f..3a74378744 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -99,7 +99,7 @@ export const Steps = { export const BotStatusesCopy = { connected: formatMessage('Running'), - publishing: formatMessage('Publishing'), + publishing: formatMessage('Building'), published: formatMessage('Starting'), unConnected: formatMessage('Inactive'), failed: formatMessage('Failed to start'), diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index e97024347a..289be16b33 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -48,6 +48,7 @@ import { showCreateDialogModalState, showAddSkillDialogModalState, localeState, + botProjectSpaceSelector, } from '../../recoilModel'; import ImportQnAFromUrlModal from '../knowledge-base/ImportQnAFromUrlModal'; import { triggerNotSupported } from '../../utils/dialogValidator'; @@ -61,7 +62,6 @@ import { deleteDialogContent, editorContainer, editorWrapper, - manifestUrl, pageRoot, visualPanel, } from './styles'; @@ -123,6 +123,7 @@ const DesignPage: React.FC { - addExistingSkillToBotProject('/Users/srravich/Desktop/EchoBot-new'); - }, - }, { 'data-testid': 'createNewSkill', key: 'createNewSkill', @@ -369,23 +348,57 @@ const DesignPage: React.FC { + const matchedProject: any = botProjectsSpace[botProjectsSpace.length - 1]; + removeSkillFromBotProject(matchedProject.projectId); + }, + }, { 'data-testid': 'AddLocalSkill', key: 'addLocalSkill', - text: formatMessage(`Add local skill from path`, { + text: formatMessage(`Add Google Keep Skill`, { displayName: currentDialog?.displayName ?? '', }), onClick: () => { - addExistingSkillToBotProject('/Users/srravich/Desktop/skill1'); + addExistingSkillToBotProject('/Users/srravich/Desktop/samples/Archive/GoogleKeepSync'); }, }, + { + 'data-testid': 'AddLocalSkill-1', + key: 'addLocalSkill', + text: formatMessage(`Add Todo Skill`, { + displayName: currentDialog?.displayName ?? '', + }), + onClick: () => { + addExistingSkillToBotProject('/Users/srravich/Desktop/samples/Archive/Todo-Skill'); + }, + }, + ], + }, + }, + { + type: 'dropdown', + text: formatMessage('Edit'), + align: 'left', + dataTestid: 'EditFlyout', + buttonProps: { + iconProps: { iconName: 'Edit' }, + }, + menuProps: { + items: [ { key: 'edit.undo', text: formatMessage('Undo'), diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index c51b9c8cbe..24bbdf5510 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -97,11 +97,12 @@ export const botProjectFileDispatcher = () => { ); const updateSkillsData = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ( + (callbackHelpers: CallbackInterface) => async ( skillNameIdentifier: string, skillsData: Skill, selectedEndpointIndex: number ) => { + const { set, snapshot } = callbackHelpers; const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); if (!rootBotProjectId) { return; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts index 7dae4460d9..4a5c46ee80 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts @@ -99,6 +99,28 @@ export const settingsDispatcher = () => { } ); + const setQnASettings = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (projectId: string, subscriptionKey: string) => { + const { set } = callbackHelpers; + try { + const response = await httpClient.post(`/projects/${projectId}/qnaSettings/set`, { + projectId, + subscriptionKey, + }); + settingStorage.setField(projectId, 'qna.endpointKey', response.data); + set(settingsState(projectId), (currentValue) => ({ + ...currentValue, + qna: { + ...currentValue.qna, + endpointKey: response.data, + }, + })); + } catch (err) { + setError(callbackHelpers, err); + } + } + ); + return { setSettings, setRuntimeSettings, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts index 70dba441a8..5bbc05f372 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts @@ -4,17 +4,62 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { SkillManifestFile } from '@bfc/shared'; +import produce from 'immer'; import { dispatcherState } from '../DispatcherWrapper'; - +import { rootBotProjectIdSelector, skillsStateSelector } from '../selectors'; import { skillManifestsState, onAddSkillDialogCompleteState, showAddSkillDialogModalState, displaySkillManifestState, -} from './../atoms/botState'; + botProjectFileState, + settingsState, + botEndpointsState, +} from '../atoms'; + +import { setSettingState } from './setting'; export const skillDispatcher = () => { + const updateSettingForLocalEndpointSkills = useRecoilCallback((callbackHelpers: CallbackInterface) => async () => { + const { snapshot } = callbackHelpers; + const botEndpoints = await snapshot.getPromise(botEndpointsState); + const skills = await snapshot.getPromise(skillsStateSelector); + const rootBotId = await snapshot.getPromise(rootBotProjectIdSelector); + if (!rootBotId) { + return; + } + + const settings = await snapshot.getPromise(settingsState(rootBotId)); + let updatedSettings = { ...settings }; + const botProjectFile = await snapshot.getPromise(botProjectFileState(rootBotId)); + if (!botProjectFile) { + return; + } + + debugger; + for (const skillNameIdentifier in botProjectFile.content.skills) { + const botProjectSkill = botProjectFile.content.skills[skillNameIdentifier]; + const projectId = skills[skillNameIdentifier]?.id; + const currentSetting = await snapshot.getPromise(settingsState(projectId)); + + // Update settings only for skills that have chosen the local endpoint + if (projectId && botEndpoints[projectId] && !botProjectSkill.endpointName) { + updatedSettings = produce(updatedSettings, (draftState) => { + if (!draftState.skill) { + draftState.skill = {}; + } + + draftState.skill[skillNameIdentifier] = { + endpointUrl: botEndpoints[projectId], + msAppId: currentSetting.MicrosoftAppId ?? '', + }; + }); + } + } + setSettingState(callbackHelpers, rootBotId, updatedSettings); + }); + const createSkillManifest = async (callbackHelpers: CallbackInterface, { id, content, projectId }) => { const { set, snapshot } = callbackHelpers; let manifestForBotProjectFile = undefined; @@ -46,11 +91,12 @@ export const skillDispatcher = () => { ); const updateSkillManifest = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ({ id, content }: SkillManifestFile, projectId: string) => { + (callbackHelpers: CallbackInterface) => async ({ id, content }: SkillManifestFile, projectId: string) => { + const { set, snapshot } = callbackHelpers; const manifests = await snapshot.getPromise(skillManifestsState(projectId)); const dispatcher = await snapshot.getPromise(dispatcherState); if (!manifests.some((manifest) => manifest.id === id)) { - createSkillManifest({ set, snapshot }, { id, content, projectId }); + createSkillManifest(callbackHelpers, { id, content, projectId }); dispatcher.updateManifestInBotProjectFile(projectId, id); return; } @@ -87,5 +133,6 @@ export const skillDispatcher = () => { addSkillDialogCancel, displayManifestModal, dismissManifestModal, + updateSettingForLocalEndpointSkills, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 752957f452..da616e07bf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -447,10 +447,7 @@ const handleSkillLoadingFailure = (callbackHelpers, { ex, skillNameIdentifier }) return projectId; }; -const migrateSkillsForExistingBots = ( - botProjectFile: BotProjectSpace, - rootBotSkill: SkillSetting & { manifestUrl: string } -) => { +const migrateSkillsForExistingBots = (botProjectFile: BotProjectSpace, rootBotSkill: any) => { if (Object.keys(botProjectFile.skills).length === 0 && Object.keys(rootBotSkill).length > 0) { for (const skillName in rootBotSkill) { const currentSkill = rootBotSkill[skillName]; @@ -488,8 +485,14 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; - const updatedFileContent = migrateSkillsForExistingBots(currentBotProjectFileIndexed.content, mergedSettings.skill); - currentBotProjectFileIndexed.content = updatedFileContent; + if (mergedSettings.skill) { + const updatedFileContent = migrateSkillsForExistingBots( + currentBotProjectFileIndexed.content, + mergedSettings.skill + ); + currentBotProjectFileIndexed.content = updatedFileContent; + } + const currentBotProjectFile: BotProjectSpace = currentBotProjectFileIndexed.content; set(botProjectFileState(rootBotProjectId), currentBotProjectFileIndexed); diff --git a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts index 6648ff714f..9489516a68 100644 --- a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts +++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts @@ -7,13 +7,14 @@ import { selector, selectorFamily } from 'recoil'; import settingsStorage from '../../utils/dialogSettingStorage'; import { BotStatus } from '../../constants'; import { isAbsHosted } from '../../utils/envUtil'; -import { botStatusState, luFilesState, qnaFilesState, settingsState } from '../atoms'; +import { botDisplayNameState, botStatusState, luFilesState, qnaFilesState, settingsState } from '../atoms'; import { Dispatcher } from '../dispatchers'; import { dispatcherState } from '../DispatcherWrapper'; import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil'; import { validateDialogSelectorFamily } from './validatedDialogs'; -import { localBotsWithoutErrorsSelector } from './project'; + +import { localBotsWithoutErrorsSelector } from '.'; export const trackBotStatusesSelector = selectorFamily({ key: 'trackBotStatusesSelector', @@ -65,7 +66,8 @@ export const buildConfigurationSelector = selector({ const localProjects = get(localBotsWithoutErrorsSelector); return localProjects.map((projectId: string) => { const result = get(buildEssentialsSelector(projectId)); - return result; + const name = get(botDisplayNameState(projectId)); + return { ...result, name }; }); }, }); diff --git a/Composer/packages/client/src/recoilModel/selectors/skills.ts b/Composer/packages/client/src/recoilModel/selectors/skills.ts index 8533ad9ca3..abc3c8de86 100644 --- a/Composer/packages/client/src/recoilModel/selectors/skills.ts +++ b/Composer/packages/client/src/recoilModel/selectors/skills.ts @@ -37,7 +37,6 @@ export const skillsStateSelector = selector({ remote: isRemote, }; } - console.log('RESULT', result); return result; }, {}); return skills; diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 2ea85e780d..fe820e5847 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -28,7 +28,7 @@ import { rateInfoState, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; -import { skillsStateSelector } from '../recoilModel/selectors'; +import { rootBotProjectIdSelector, skillsStateSelector } from '../recoilModel/selectors'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -58,6 +58,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); From bd580f99d0920be3e8b732f794dd453036d2f16d Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 19 Oct 2020 15:36:36 -0700 Subject: [PATCH 044/100] Start bots panel first pass completed Signed-off-by: Srinaath Ravichandran --- .../packages/client/src/components/Header.tsx | 62 ++++++++++++++----- .../TestController/useLocalBotOperations.ts | 17 +++-- .../client/src/pages/design/DesignPage.tsx | 5 -- .../src/pages/knowledge-base/QnAPage.tsx | 5 -- .../src/pages/language-generation/LGPage.tsx | 11 +--- .../pages/language-understanding/LUPage.tsx | 11 +--- .../client/src/pages/setting/SettingsPage.tsx | 6 -- .../client/src/pages/skills/index.tsx | 6 -- .../src/recoilModel/dispatchers/publisher.ts | 6 ++ .../recoilModel/dispatchers/utils/project.ts | 12 +++- .../selectors/localRuntimeBuilder.ts | 2 + .../src/recoilModel/selectors/project.ts | 1 + .../lib/shared/src/skillsUtils/index.ts | 2 +- .../src/SkillEndpointField.tsx | 20 ++++-- Composer/plugins/localPublish/src/index.ts | 48 ++++++++------ 15 files changed, 123 insertions(+), 91 deletions(-) diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index f56b716bd1..0a94282838 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -21,13 +21,13 @@ import { } from '../recoilModel'; import composerIcon from '../images/composerIcon.svg'; import { AppUpdaterStatus } from '../constants'; +import { useLocation } from '../utils/hooks'; import { StartBotsPanel } from './TestController/startBotsPanel'; import { useBotOperations } from './TestController/useLocalBotOperations'; export const actionButton = css` font-size: 18px; margin-top: 2px; - color: #fff; `; // -------------------- Styles -------------------- // @@ -102,9 +102,17 @@ const botRuntimeStartIcon: IButtonStyles = { }, }; +const startBotWidgetContainer = css` + display: flex; + align-items: center; + justify-content: center; + color: #fff; +`; + // -------------------- Header -------------------- // export const Header = () => { + const currentLocation = useLocation(); const { setAppUpdateShowing } = useRecoilValue(dispatcherState); const projectId = useRecoilValue(currentProjectIdState); const projectName = useRecoilValue(botDisplayNameState(projectId)); @@ -114,8 +122,25 @@ export const Header = () => { const [showStartBotsPanel, setStartPanelView] = useState(false); const runningBots = useRecoilValue(runningBotsSelector); const [startPanelText, setStartPanelText] = useState(''); - const [allBotsStarted, setAllBotsStarted] = useState(false); - const { stopAllBots, startAllBots } = useBotOperations(setAllBotsStarted); + const { projectIds: runningProjects } = useRecoilValue(runningBotsSelector); + const [areBotsStarted, setBotsInBotProjectStarted] = useState(false); + const { stopAllBots, startAllBots } = useBotOperations(); + const [showStartBotsWidget, setStartBotsWidgetVisible] = useState(true); + + useEffect(() => { + // TODO: Start using modes to detect current page after #4361 + if (!currentLocation.location.pathname.includes('home')) { + setStartBotsWidgetVisible(true); + return; + } + setStartBotsWidgetVisible(false); + }, [currentLocation]); + + useEffect(() => { + if (runningProjects.length > 0) { + setBotsInBotProjectStarted(true); + } + }, [runningProjects]); useEffect(() => { if (runningBots.projectIds.length > 0) { @@ -138,10 +163,10 @@ export const Header = () => { } function handleStartOrStopAll() { - if (allBotsStarted) { + if (areBotsStarted) { stopAllBots(); dismissStartPanelViewer(); - setAllBotsStarted(false); + setBotsInBotProjectStarted(false); } else { startAllBots(); setStartPanelView(true); @@ -169,18 +194,23 @@ export const Header = () => {
- {runningBots.projectIds.length > 0 ? ( - - - - ) : ( - - - + {showStartBotsWidget && ( +
+ {runningBots.projectIds.length > 0 ? ( + + + + ) : ( + + + + )} + {startPanelText} + setStartPanelView(true)}> + + +
)} - setStartPanelView(true)}> - {startPanelText} - {showUpdateAvailableIcon && ( void) const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector); const rootBotId = useRecoilValue(rootBotProjectIdSelector); const [trackedProjectIds, setProjectsToTrack] = useState([]); - const { updateSettingForLocalEndpointSkills } = useRecoilValue(dispatcherState); + const { updateSettingForLocalEndpointSkills, resetBotRuntimeError } = useRecoilValue(dispatcherState); const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { if (botBuildRequired) { @@ -35,8 +36,11 @@ export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) setProjectsToTrack([]); await updateSettingForLocalEndpointSkills(); const rootBot = builderEssentials[0]; - const { projectId, configuration, buildRequired } = rootBot; - handleBotStart(projectId, configuration, buildRequired); + const { projectId, configuration, buildRequired, status } = rootBot; + if (status !== BotStatus.connected) { + resetBotRuntimeError(projectId); + handleBotStart(projectId, configuration, buildRequired); + } }; // Custom hook to make sure root bot is started after all skills have been started. @@ -49,8 +53,11 @@ export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) const trackProjects: string[] = skillsBots.map((skillBot) => skillBot.projectId); setProjectsToTrack(trackProjects); for (const botBuildConfig of skillsBots) { - const { projectId, configuration, buildRequired } = botBuildConfig; - await handleBotStart(projectId, configuration, buildRequired); + if (botBuildConfig.status !== BotStatus.connected) { + const { projectId, configuration, buildRequired } = botBuildConfig; + resetBotRuntimeError(projectId); + await handleBotStart(projectId, configuration, buildRequired); + } } if (onAllBotsStarted) { onAllBotsStarted(true); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 289be16b33..39cc27cbd7 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -501,11 +501,6 @@ const DesignPage: React.FC, - align: 'right', - }, ]; function handleBreadcrumbItemClick(_event, item) { diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 8381f84378..39ee8cb781 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -114,11 +114,6 @@ const QnAPage: React.FC = (props) => { ], }, }, - { - type: 'element', - element: , - align: 'right', - }, ]; const onRenderHeaderContent = () => { diff --git a/Composer/packages/client/src/pages/language-generation/LGPage.tsx b/Composer/packages/client/src/pages/language-generation/LGPage.tsx index d786f33b01..480ac3d1bf 100644 --- a/Composer/packages/client/src/pages/language-generation/LGPage.tsx +++ b/Composer/packages/client/src/pages/language-generation/LGPage.tsx @@ -12,7 +12,6 @@ import { useRecoilValue } from 'recoil'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { actionButton } from '../language-understanding/styles'; import { navigateTo } from '../../utils/navigation'; -import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; import { validateDialogSelectorFamily } from '../../recoilModel'; @@ -82,14 +81,6 @@ const LGPage: React.FC> = (props: RouteComponen [dialogId, projectId] ); - const toolbarItems = [ - { - type: 'element', - element: , - align: 'right', - }, - ]; - const onRenderHeaderContent = () => { return ( > = (props: RouteComponen navLinks={navLinks} navRegionName={formatMessage('LG Navigation Pane')} title={formatMessage('Bot Responses')} - toolbarItems={toolbarItems} + toolbarItems={} onRenderHeaderContent={onRenderHeaderContent} > }> diff --git a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx index 4cad612016..ec0ead0575 100644 --- a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx +++ b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx @@ -10,7 +10,6 @@ import { useRecoilValue } from 'recoil'; import { navigateTo } from '../../utils/navigation'; import { LoadingSpinner } from '../../components/LoadingSpinner'; -import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; import { validateDialogSelectorFamily } from '../../recoilModel'; @@ -74,14 +73,6 @@ const LUPage: React.FC, - align: 'right', - }, - ]; - const onRenderHeaderContent = () => { if (!isRoot || edit) { return ( @@ -106,7 +97,7 @@ const LUPage: React.FC }> diff --git a/Composer/packages/client/src/pages/setting/SettingsPage.tsx b/Composer/packages/client/src/pages/setting/SettingsPage.tsx index 37b12d8b4b..38c81cc00b 100644 --- a/Composer/packages/client/src/pages/setting/SettingsPage.tsx +++ b/Composer/packages/client/src/pages/setting/SettingsPage.tsx @@ -215,12 +215,6 @@ const SettingPage: React.FC = () => { dataTestid: 'AddLanguageFlyout', disabled: false, }, - - { - type: 'element', - element: , - align: 'right', - }, ]; const title = useMemo(() => { diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx index 12df39ccaf..7f2ca848fa 100644 --- a/Composer/packages/client/src/pages/skills/index.tsx +++ b/Composer/packages/client/src/pages/skills/index.tsx @@ -11,7 +11,6 @@ import { SkillSetting } from '@bfc/shared'; import { dispatcherState, settingsState, botDisplayNameState } from '../../recoilModel'; import { Toolbar, IToolbarItem } from '../../components/Toolbar'; -import { TestController } from '../../components/TestController/TestController'; import { CreateSkillModal } from '../../components/CreateSkillModal'; import { ContainerStyle, ContentHeaderStyle, HeaderText } from './styles'; @@ -40,11 +39,6 @@ const Skills: React.FC> = (props) => }, align: 'left', }, - { - type: 'element', - element: , - align: 'right', - }, ]; const onSubmitForm = useCallback( diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index dd2585c40c..251c9ff619 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -220,6 +220,11 @@ export const publisherDispatcher = () => { } ); + const resetBotRuntimeError = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { + const { reset } = callbackHelpers; + reset(botRuntimeErrorState(projectId)); + }); + const openBotInEmulator = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { const { snapshot } = callbackHelpers; const botEndpoints = await snapshot.getPromise(botEndpointsState); @@ -246,5 +251,6 @@ export const publisherDispatcher = () => { getPublishHistory, setEjectRuntimeExist, openBotInEmulator, + resetBotRuntimeError, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index da616e07bf..2189f02995 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -17,6 +17,7 @@ import { LuFile, QnAFile, SensitiveProperties, + defaultPublishConfig, } from '@bfc/shared'; import formatMessage from 'format-message'; import camelCase from 'lodash/camelCase'; @@ -69,7 +70,7 @@ import lgWorker from '../../parsers/lgWorker'; import luWorker from '../../parsers/luWorker'; import qnaWorker from '../../parsers/qnaWorker'; import FilePersistence from '../../persistence/FilePersistence'; -import { rootBotProjectIdSelector } from '../../selectors'; +import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../../selectors'; import { undoHistoryState } from '../../undo/history'; import UndoHistory from '../../undo/undoHistory'; import { logMessage, setError } from '../shared'; @@ -97,12 +98,15 @@ export const setErrorOnBotProject = async ( if (payload != null) logMessage(callbackHelpers, `Error loading ${botName}: ${JSON.stringify(payload)}`); }; -export const flushExistingTasks = async (callbackHelpers) => { +export const flushExistingTasks = async (callbackHelpers: CallbackInterface) => { const { snapshot, reset } = callbackHelpers; reset(botProjectSpaceLoadedState); const projectIds = await snapshot.getPromise(botProjectIdsState); - reset(botProjectIdsState, []); + const runtimeOperations = await snapshot.getPromise(botRuntimeOperationsSelector); + + reset(botProjectIdsState); for (const projectId of projectIds) { + await runtimeOperations?.stopBot(projectId); resetBotStates(callbackHelpers, projectId); } const workers = [lgWorker, luWorker, qnaWorker]; @@ -482,6 +486,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st set(botNameIdentifierState(rootBotProjectId), camelCase(name)); set(botProjectIdsState, [rootBotProjectId]); + dispatcher.getPublishStatus(rootBotProjectId, defaultPublishConfig); if (botFiles.botProjectSpaceFiles && botFiles.botProjectSpaceFiles.length) { const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; @@ -520,6 +525,7 @@ const openRootBotAndSkills = async (callbackHelpers: CallbackInterface, data, st if (matchedEndpoint) { dispatcher.updateEndpointNameInBotProjectFile(nameIdentifier, matchedEndpoint); } + dispatcher.getPublishStatus(projectId, defaultPublishConfig); }) .catch((ex) => { const projectId = handleSkillLoadingFailure(callbackHelpers, { diff --git a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts index 9489516a68..d5d6b4509b 100644 --- a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts +++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts @@ -50,12 +50,14 @@ export const buildEssentialsSelector = selectorFamily({ const luFiles = get(luFilesState(projectId)); const qnaFiles = get(qnaFilesState(projectId)); const botBuildRequired = get(botBuildRequiredSelector(projectId)); + const status = get(botStatusState(projectId)); return { isConfigurationComplete: isBuildConfigurationComplete(configuration, dialogs, luFiles, qnaFiles), configuration, buildRequired: botBuildRequired, projectId, + status, }; }, }); diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index f4c5b2c973..3b0dfb4831 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -57,6 +57,7 @@ export const botProjectSpaceSelector = selector({ return { dialogs, projectId, name, ...metaData, error: botError, botNameId }; }); + console.log('RESULT', result); return result; }, }); diff --git a/Composer/packages/lib/shared/src/skillsUtils/index.ts b/Composer/packages/lib/shared/src/skillsUtils/index.ts index ad7b0bccdf..c1d451da9d 100644 --- a/Composer/packages/lib/shared/src/skillsUtils/index.ts +++ b/Composer/packages/lib/shared/src/skillsUtils/index.ts @@ -9,7 +9,7 @@ import camelCase from 'lodash/camelCase'; export const VIRTUAL_LOCAL_ENDPOINT = { key: -1, - name: formatMessage('Local Endpoint'), + name: formatMessage('Local Composer'), }; export function fetchFromSettings(path: string, settings: DialogSetting): string { diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx index 6698d62af7..38f87184cf 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx @@ -21,7 +21,7 @@ export const SkillEndpointField: React.FC = (props) => { const endpoints = skill?.manifest?.endpoints || []; const options = useMemo(() => { - const endpointsInManifest = endpoints.map(({ name, endpointUrl, msAppId }, key) => ({ + const endpointsInManifest: any[] = endpoints.map(({ name, endpointUrl, msAppId }, key) => ({ key, text: name, data: { @@ -35,6 +35,11 @@ export const SkillEndpointField: React.FC = (props) => { let localEndpoint: any[] = []; if (!skill.remote) { localEndpoint = [ + { + key: 'localEndpointHeader', + itemType: SelectableOptionMenuItemType.Header, + text: 'Local Endpoints', + }, { key: -1, text: VIRTUAL_LOCAL_ENDPOINT.name, @@ -44,13 +49,16 @@ export const SkillEndpointField: React.FC = (props) => { name: VIRTUAL_LOCAL_ENDPOINT.name, }, }, - { - key: 'separator', - itemType: SelectableOptionMenuItemType.Divider, - text: '', - }, ]; } + if (endpointsInManifest.length > 0) { + endpointsInManifest.unshift({ + key: 'remoteEndpointHeader', + itemType: SelectableOptionMenuItemType.Header, + text: 'Manifest Endpoints', + }); + } + return [...localEndpoint, ...endpointsInManifest]; }, [endpoints]); diff --git a/Composer/plugins/localPublish/src/index.ts b/Composer/plugins/localPublish/src/index.ts index a852718355..80d7379ead 100644 --- a/Composer/plugins/localPublish/src/index.ts +++ b/Composer/plugins/localPublish/src/index.ts @@ -10,7 +10,6 @@ import rimraf from 'rimraf'; import archiver from 'archiver'; import { v4 as uuid } from 'uuid'; import AdmZip from 'adm-zip'; -import portfinder from 'portfinder'; import killPort from 'kill-port'; import map from 'lodash/map'; import range from 'lodash/range'; @@ -90,7 +89,7 @@ class LocalPublisher { } await this.setBot(botId, version, fullSettings, project); } catch (error) { - this.stopBot(botId); + await this.stopBot(botId); this.setBotStatus(botId, { status: 500, result: { @@ -151,6 +150,7 @@ class LocalPublisher { }; if (LocalPublisher.runningBots[botId].status === 500) { // after we return the 500 status once, delete it out of the running bots list. + await this.stopBot(botId); delete LocalPublisher.runningBots[botId]; } return status; @@ -216,6 +216,7 @@ class LocalPublisher { const botId = project.id; const isExist = await this.botExist(botId); // get runtime template + const runtime = this.composer.getRuntimeByProject(project); try { if (!isExist) { @@ -235,7 +236,7 @@ class LocalPublisher { await runtime.build(runtimeDir, project); } else { // stop bot - this.stopBot(botId); + await this.stopBot(botId); // get previous settings // when changing type of runtime const settings = JSON.parse( @@ -280,7 +281,7 @@ class LocalPublisher { this.composer.log('Bot already running. Stopping bot...'); // this may or may not be set based on the status of the bot port = LocalPublisher.runningBots[botId].port; - this.stopBot(botId); + await this.stopBot(botId); } if (!port) { port = await getPort({ port: this.getAvailablePorts() }); @@ -306,7 +307,7 @@ class LocalPublisher { await this.startBot(botId, port, settings, project); } catch (error) { console.error('Error in startbot: ', error); - this.stopBot(botId); + await this.stopBot(botId); this.setBotStatus(botId, { status: 500, result: { @@ -344,18 +345,19 @@ class LocalPublisher { } ); this.composer.log('Started process %d', spawnProcess.pid); + this.setBotStatus(botId, { + process: spawnProcess, + port: port, + status: 200, + result: { message: 'Runtime started' }, + }); + const processLog = this.composer.log.extend(spawnProcess.pid); + this.addListeners(spawnProcess, botId, processLog); + resolve(); } catch (err) { - return reject(err); + reject(err); + throw err; } - this.setBotStatus(botId, { - process: spawnProcess, - port: port, - status: 200, - result: { message: 'Runtime started' }, - }); - const processLog = this.composer.log.extend(spawnProcess.pid); - this.addListeners(spawnProcess, botId, processLog); - resolve(); }); }; @@ -465,10 +467,20 @@ class LocalPublisher { if (port) { this.composer.log('Killing process at port %d', port); - this.removeListener(proc); - await killPort(port); + await new Promise((resolve, reject) => { + setTimeout(async () => { + killPort(port) + .then(() => { + this.removeListener(proc); + delete LocalPublisher.runningBots[botId]; + resolve(); + }) + .catch((err) => { + reject(err); + }); + }, 1000); + }); } - delete LocalPublisher.runningBots[botId]; }; private copyDir = async (srcDir: string, dstDir: string) => { From e6ef1d2fcadb25f7d5ba7f69ab5294f0557c42d3 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 19 Oct 2020 22:50:36 -0700 Subject: [PATCH 045/100] Updated tests for removing workspace reference Signed-off-by: Srinaath Ravichandran --- .../__tests__/botProjectFile.test.tsx | 12 ++++---- .../dispatchers/__tests__/project.test.tsx | 23 ++++----------- .../src/recoilModel/dispatchers/project.ts | 2 +- .../shared/__tests__/fileUtils/index.test.ts | 28 ------------------- .../packages/server/schemas/botproject.schema | 2 +- .../models/bot/__tests__/botProject.test.ts | 1 - .../server/src/models/bot/botProject.ts | 1 - 7 files changed, 15 insertions(+), 54 deletions(-) delete mode 100644 Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts 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 678d906e3d..efe979e14f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx @@ -87,7 +87,6 @@ describe('Bot Project File dispatcher', () => { content: { $schema: '', name: 'TesterBot', - workspace: 'file:///Users/tester/Desktop/LoadedBotProject/TesterBot', skills: {}, }, }, @@ -99,6 +98,10 @@ describe('Bot Project File dispatcher', () => { }, }, { recoilState: botProjectIdsState, initialValue: [rootBotProjectId] }, + { + recoilState: locationState(rootBotProjectId), + initialValue: '/Users/tester/Desktop/LoadedBotProject/RootBot', + }, ], dispatcher: { recoilState: dispatcherState, @@ -115,7 +118,7 @@ describe('Bot Project File dispatcher', () => { it('should add a local skill to bot project file', async () => { await act(async () => { renderedComponent.current.setSkillsData({ - location: 'Users/tester/Desktop/LoadedBotProject/Todo-Skill', + location: '/Users/tester/Desktop/LoadedBotProject/Todo-Skill', botNameIdentifier: 'todoSkill', }); }); @@ -124,9 +127,7 @@ describe('Bot Project File dispatcher', () => { dispatcher.addLocalSkillToBotProjectFile(testSkillId); }); - expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toBe( - 'file:///Users/tester/Desktop/LoadedBotProject/Todo-Skill' - ); + expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.workspace).toBe('../Todo-Skill'); expect(renderedComponent.current.botProjectFile.content.skills.todoSkill.remote).toBeFalsy(); }); @@ -160,6 +161,7 @@ describe('Bot Project File dispatcher', () => { await act(async () => { dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); }); + console.log('HITTTT ME', renderedComponent.current.botProjectFile.content.skills); expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); await act(async () => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index b29387b55a..11941d6515 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -363,7 +363,7 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.botStates.echoSkill2.botDisplayName).toBe('Echo-Skill-2'); await act(async () => { - await dispatcher.addRemoteSkillToBotProject('https://test.net/api/manifest/man', 'test-skill', 'remote'); + await dispatcher.addRemoteSkillToBotProject('https://test.net/api/manifest/test', 'remote'); }); expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); @@ -392,14 +392,12 @@ describe('Project dispatcher', () => { await act(async () => { await dispatcher.addRemoteSkillToBotProject( 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json', - 'one-note', 'remote' ); }); - - expect(renderedComponent.current.botStates.oneNote).toBeDefined(); - expect(renderedComponent.current.botStates.oneNote.botDisplayName).toBe('OneNoteSync'); - expect(renderedComponent.current.botStates.oneNote.location).toBe( + expect(renderedComponent.current.botStates.oneNoteSync).toBeDefined(); + expect(renderedComponent.current.botStates.oneNoteSync.botDisplayName).toBe('OneNoteSync'); + expect(renderedComponent.current.botStates.oneNoteSync.location).toBe( 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json' ); expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/emptybot-1`); @@ -429,26 +427,17 @@ describe('Project dispatcher', () => { await act(async () => { await dispatcher.addRemoteSkillToBotProject( 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json', - 'one-note', - 'remote' - ); - }); - - await act(async () => { - await dispatcher.addRemoteSkillToBotProject( - 'https://test-dev.azurewebsites.net/manifests/onenote-second-manifest.json', - 'one-note-2', 'remote' ); }); - const oneNoteProjectId = renderedComponent.current.botStates.oneNote.projectId; + const oneNoteProjectId = renderedComponent.current.botStates.oneNoteSync.projectId; mockImplementation.mockClear(); await act(async () => { dispatcher.removeSkillFromBotProject(oneNoteProjectId); }); - expect(renderedComponent.current.botStates.oneNote).toBeUndefined(); + expect(renderedComponent.current.botStates.oneNoteSync).toBeUndefined(); }); it('should be able to add a new skill to Botproject', async () => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 2f54389453..404824e3bd 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -114,7 +114,7 @@ export const projectDispatcher = () => { ); const addRemoteSkillToBotProject = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (manifestUrl: string, name: string, endpointName: string) => { + (callbackHelpers: CallbackInterface) => async (manifestUrl: string, endpointName: string) => { const { set, snapshot } = callbackHelpers; try { const dispatcher = await snapshot.getPromise(dispatcherState); diff --git a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts b/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts deleted file mode 100644 index 124c0e62cf..0000000000 --- a/Composer/packages/lib/shared/__tests__/fileUtils/index.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { convertAbsolutePathToFileProtocol, convertFileProtocolToPath } from '../../src/fileUtils'; - -it('should convert a posix path to file protocol', () => { - const testPath = '/Users/tester/empty-bot-0'; - expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///Users/tester/empty-bot-0'); -}); - -it('should convert a windows path to file protocol', () => { - const testPath = 'C:/Users/Tester/empty-bot-0'; - expect(convertAbsolutePathToFileProtocol(testPath)).toBe('file:///C:/Users/Tester/empty-bot-0'); -}); - -it('should convert a Windows file protocol path to regular path', () => { - const testPath = 'file:///C:/Users/Tester/empty-bot-0'; - expect(convertFileProtocolToPath(testPath)).toBe('C:/Users/Tester/empty-bot-0'); -}); - -it('should convert a Mac file protocol path to regular path', () => { - const testPath = 'file:///users/tester/empty-bot-0'; - expect(convertFileProtocolToPath(testPath)).toBe('/users/tester/empty-bot-0'); -}); - -it('should give empty string if path is not available', () => { - const testPath = ''; - expect(convertFileProtocolToPath(testPath)).toBe(''); -}); diff --git a/Composer/packages/server/schemas/botproject.schema b/Composer/packages/server/schemas/botproject.schema index fe45b61f18..c774c0334b 100644 --- a/Composer/packages/server/schemas/botproject.schema +++ b/Composer/packages/server/schemas/botproject.schema @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://github.com/microsoft/BotFramework-Composer/blob/main/Composer/packages/server/schemas/botproject.schema", - "$version": "1.0.0", + "$version": "0.2.0", "title": "Root Bot and Skills workspace schema for a environment. Each publishing environment has a Bot Project file associated with it.", "description": "Schema that captures the relationship between the Root Bot and remote/local skills that the Root Bot consumes.", "type": "object", 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 ead0964b84..f12cda812b 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -21,7 +21,6 @@ jest.mock('../../../services/asset', () => { botProjectFileTemplate: { $schema: '', name: '', - workspace: '', skills: {}, }, }, diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index da1b18da96..fccd0cee6b 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -363,7 +363,6 @@ export class BotProject implements IBotProject { for (const botProjectFile of this.botProjectFiles) { const { relativePath } = botProjectFile; const content = JSON.parse(botProjectFile.content); - content.workspace = this.dataDir; content.name = botName; await this._updateFile(relativePath, JSON.stringify(content, null, 2)); } From 1339b1504033e5ea61910cc7e7d1589ee8c2b126 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 19 Oct 2020 22:54:18 -0700 Subject: [PATCH 046/100] Removed manifest update operation Signed-off-by: Srinaath Ravichandran --- .../recoilModel/dispatchers/botProjectFile.ts | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts index c1104cf171..cb0e937d8e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/botProjectFile.ts @@ -71,34 +71,9 @@ export const botProjectFileDispatcher = () => { }); }); - const updateManifest = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async (skillProjectId: string, manifestId: string | undefined) => { - const rootBotProjectId = await snapshot.getPromise(rootBotProjectIdSelector); - if (!rootBotProjectId) { - return; - } - const skillNameIdentifier = await snapshot.getPromise(botNameIdentifierState(skillProjectId)); - - set(botProjectFileState(rootBotProjectId), (current) => { - const result = produce(current, (draftState) => { - if (!manifestId) { - delete draftState[skillNameIdentifier].manifest; - } else { - draftState[skillNameIdentifier] = { - ...draftState[skillNameIdentifier], - manifest: manifestId, - }; - } - }); - return result; - }); - } - ); - return { addLocalSkillToBotProjectFile: addLocalSkill, removeSkillFromBotProjectFile: removeSkill, addRemoteSkillToBotProjectFile: addRemoteSkill, - updateManifestInBotProjectFile: updateManifest, }; }; From 64cc8473097000a5b77bb9060f95ebf793769c35 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Mon, 19 Oct 2020 23:08:28 -0700 Subject: [PATCH 047/100] Cleared logs Signed-off-by: Srinaath Ravichandran --- .../recoilModel/dispatchers/__tests__/botProjectFile.test.tsx | 1 - 1 file changed, 1 deletion(-) 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 efe979e14f..8d24ddeb21 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/botProjectFile.test.tsx @@ -161,7 +161,6 @@ describe('Bot Project File dispatcher', () => { await act(async () => { dispatcher.addRemoteSkillToBotProjectFile(testSkillId, manifestUrl, 'remote'); }); - console.log('HITTTT ME', renderedComponent.current.botProjectFile.content.skills); expect(renderedComponent.current.botProjectFile.content.skills.oneNoteSkill.manifest).toBe(manifestUrl); await act(async () => { From c4b33ff07b9211f9a8f094f8ce720c774644e8e9 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 20 Oct 2020 12:38:00 -0700 Subject: [PATCH 048/100] Remove skill references Signed-off-by: Srinaath Ravichandran --- .../client/src/pages/skills/index.tsx | 81 ------- .../client/src/pages/skills/skill-list.tsx | 200 ------------------ .../src/pages/skills/skill-settings.tsx | 97 --------- .../client/src/pages/skills/styles.ts | 72 ------- .../dispatchers/__tests__/skill.test.ts | 71 ------- Composer/packages/client/src/router.tsx | 2 - .../packages/client/src/shell/useShell.ts | 3 +- 7 files changed, 1 insertion(+), 525 deletions(-) delete mode 100644 Composer/packages/client/src/pages/skills/index.tsx delete mode 100644 Composer/packages/client/src/pages/skills/skill-list.tsx delete mode 100644 Composer/packages/client/src/pages/skills/skill-settings.tsx delete mode 100644 Composer/packages/client/src/pages/skills/styles.ts diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx deleted file mode 100644 index 7f2ca848fa..0000000000 --- a/Composer/packages/client/src/pages/skills/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { RouteComponentProps } from '@reach/router'; -import React, { useCallback, useState } from 'react'; -import formatMessage from 'format-message'; -import { useRecoilValue } from 'recoil'; -import { SkillSetting } from '@bfc/shared'; - -import { dispatcherState, settingsState, botDisplayNameState } from '../../recoilModel'; -import { Toolbar, IToolbarItem } from '../../components/Toolbar'; -import { CreateSkillModal } from '../../components/CreateSkillModal'; - -import { ContainerStyle, ContentHeaderStyle, HeaderText } from './styles'; -import SkillSettings from './skill-settings'; -import SkillList from './skill-list'; - -const Skills: React.FC> = (props) => { - const { projectId = '' } = props; - const [showAddSkillDialogModal, setShowAddSkillDialogModal] = useState(false); - - const botName = useRecoilValue(botDisplayNameState(projectId)); - const settings = useRecoilValue(settingsState(projectId)); - const { addSkill, setSettings } = useRecoilValue(dispatcherState); - - const toolbarItems: IToolbarItem[] = [ - { - type: 'action', - text: formatMessage('Connect to a new skill'), - buttonProps: { - iconProps: { - iconName: 'Add', - }, - onClick: () => { - setShowAddSkillDialogModal(true); - }, - }, - align: 'left', - }, - ]; - - const onSubmitForm = useCallback( - (skill: SkillSetting) => { - addSkill(projectId, skill); - setShowAddSkillDialogModal(false); - }, - [projectId] - ); - - const onDismissForm = useCallback(() => { - setShowAddSkillDialogModal(false); - }, []); - - return ( -
- -
-

{formatMessage('Skills')}

-
-
- -
- - {showAddSkillDialogModal && ( - - )} -
- ); -}; - -export default Skills; diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx deleted file mode 100644 index 6fb822fbfd..0000000000 --- a/Composer/packages/client/src/pages/skills/skill-list.tsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { - DetailsList, - DetailsListLayoutMode, - SelectionMode, - CheckboxVisibility, - IColumn, -} from 'office-ui-fabric-react/lib/DetailsList'; -import React, { useState, useCallback, useMemo } from 'react'; -import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; -import { IconButton } from 'office-ui-fabric-react/lib/Button'; -import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; -import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; -import { Stack } from 'office-ui-fabric-react/lib/Stack'; -import { FontSizes } from '@uifabric/fluent-theme'; -import { useRecoilValue } from 'recoil'; -import formatMessage from 'format-message'; -import { Skill } from '@bfc/shared'; - -// import { DisplayManifestModal } from '../../components/Modal/DisplayManifestModal'; -import { dispatcherState, skillsStateSelector } from '../../recoilModel'; - -import { TableView, TableCell } from './styles'; - -const columns: IColumn[] = [ - { - key: 'name', - name: formatMessage('Available Skills'), - fieldName: 'name', - minWidth: 100, - maxWidth: 150, - isResizable: true, - data: 'string', - onRender: ({ skill: { name } }) => { - return
{name}
; - }, - }, - { - key: 'endpointUrl', - name: formatMessage('Skill Endpoint'), - fieldName: 'endpointUrl', - minWidth: 250, - maxWidth: 400, - isResizable: true, - data: 'string', - onRender: ({ skill, onEditSkill }) => { - const { endpoints, endpointUrl: selectedEndpointUrl } = skill; - - const options = (endpoints || []).map(({ name, endpointUrl, msAppId }, key) => ({ - key, - text: name, - data: { - endpointUrl, - msAppId, - }, - selected: endpointUrl === selectedEndpointUrl, - })); - - const handleChange = (_, option?: IDropdownOption) => { - if (option) { - onEditSkill({ ...skill, ...option.data }); - } - }; - - return ; - }, - }, - { - key: 'description', - name: formatMessage('Description'), - fieldName: 'description', - minWidth: 200, - maxWidth: 400, - isResizable: true, - data: 'string', - onRender: ({ skill: { description } }) => { - return
{description}
; - }, - }, - { - key: 'buttons', - name: '', - minWidth: 120, - maxWidth: 120, - fieldName: 'buttons', - data: 'string', - onRender: ({ onDelete, onViewManifest }) => { - return ( -
- - onDelete()} - /> - onViewManifest()} - /> - -
- ); - }, - }, -]; - -interface SkillListProps { - projectId: string; -} - -const SkillList: React.FC = ({ projectId }) => { - const { removeSkillFromBotProject, addRemoteSkillToBotProject } = useRecoilValue(dispatcherState); - const skills = useRecoilValue(skillsStateSelector); - - const [selectedSkillUrl, setSelectedSkillUrl] = useState(null); - - const handleViewManifest = (item) => { - if (item && item.name && item.content) { - setSelectedSkillUrl(item.manifestUrl); - } - }; - - const handleEditSkill = (projectId, skillId) => (skillData) => {}; - - const generateSkillsList = (skills: Record) => { - const skillsList: any[] = []; - for (const skillName in skills) { - const currentSkill = skills[skillName]; - skillsList.push({ - skill: { - name: currentSkill.name, - description: currentSkill.description, - onDelete: () => removeSkillFromBotProject(currentSkill.id), - onViewManifest: () => handleViewManifest(currentSkill.id), - onEditSkill: handleEditSkill(projectId, currentSkill.id), - }, - }); - } - return skillsList; - }; - - const items = useMemo(() => generateSkillsList(skills), [skills, projectId]); - - const onDismissManifest = () => { - setSelectedSkillUrl(null); - }; - - const onRenderDetailsHeader = useCallback((props, defaultRender) => { - return ( -
- - {defaultRender({ - ...props, - onRenderColumnHeaderTooltip: (tooltipHostProps) => , - })} - -
- ); - }, []); - - return ( - -
- - - -
- {/* */} -
- ); -}; - -export default SkillList; diff --git a/Composer/packages/client/src/pages/skills/skill-settings.tsx b/Composer/packages/client/src/pages/skills/skill-settings.tsx deleted file mode 100644 index cb14016c1a..0000000000 --- a/Composer/packages/client/src/pages/skills/skill-settings.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React, { useState, useEffect } from 'react'; -import formatMessage from 'format-message'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import { DialogSetting } from '@bfc/shared'; - -import { FormFieldAlignHorizontalBotSettings } from './styles'; - -export interface ISkillFormProps { - botId?: string; - skillHostEndpoint?: string; - botPassword?: string; - setSettings: (projectId: string, settings: DialogSetting) => Promise; - projectId: string; - botName: string; - settings: DialogSetting; -} - -const SkillSettings: React.FC = (props) => { - const [skillSettings, setSkillSettings] = useState({ - botId: props.botId, - skillHostEndpoint: props.skillHostEndpoint, - botPassword: props.botPassword, - }); - useEffect(() => { - setSkillSettings({ - botId: props.botId, - skillHostEndpoint: props.skillHostEndpoint, - botPassword: props.botPassword, - }); - }, [props.botId, props.botPassword, props.skillHostEndpoint]); - - const handleFieldChange = (event) => { - const localSettings = { - ...skillSettings, - [event.target.id]: event.target.value, - }; - - // auto fill `botId` with `MicrosoftAppId` - if (event.target.id === 'MicrosoftAppId') { - localSettings.botId = event.target.value; - } - - setSkillSettings({ ...localSettings }); - props.setSettings(props.projectId, { ...props.settings, ...localSettings }); - }; - - return ( -
-
- -
-
- -
-
- -
-
- ); -}; - -export default SkillSettings; diff --git a/Composer/packages/client/src/pages/skills/styles.ts b/Composer/packages/client/src/pages/skills/styles.ts deleted file mode 100644 index 9221fa0a08..0000000000 --- a/Composer/packages/client/src/pages/skills/styles.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { FontWeights, FontSizes } from 'office-ui-fabric-react/lib/Styling'; -import { css } from '@emotion/core'; - -export const ContainerStyle = css` - display: flex; - flex-direction: column; - height: 100%; -`; - -export const ContentHeaderStyle = css` - padding: 5px 20px; - height: 60px; - display: flex; - flex-shrink: 0; - justify-content: space-between; - align-items: center; -`; - -export const HeaderText = css` - font-size: ${FontSizes.xLarge}; - font-weight: ${FontWeights.semibold}; -`; - -export const ContentStyle = css` - margin-left: 2px; - display: flex; - border-top: 1px solid #dddddd; - flex: 1; - position: relative; - nav { - ul { - margin-top: 0px; - } - } -`; - -export const TableView = css` - flex: 4; - margin: 20px; - position: relative; - overflow: visible; - fontsize: 16px; -`; - -export const TableCell = css` - white-space: pre-wrap; - font-size: 14px; - textarea, - input { - border: 1px solid #dddddd; - } -`; - -export const FormFieldAlignHorizontalBotSettings = css` - max-width: 1500px; - margin-top: 20px; - align-items: flex-start; - display: flex; - flex-wrap: wrap; - flex-direction: row; - padding-bottom: 15px; -`; - -export const ManifestModalHeaderStyle = css` - margin: 14px 0 0 16px; - font-size: 20px; - font-weight: bolder; - align-items: left; -`; 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 8858f0173d..13d4928b19 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts @@ -33,20 +33,6 @@ jest.mock('../../../utils/httpUtil', () => { const mockDialogComplete = jest.fn(); const projectId = '42345.23432'; -const makeTestSkill: (number) => Skill = (n) => ({ - id: 'id' + n, - manifestUrl: 'url' + n, - name: 'skill' + n, - description: 'test skill' + n, - endpointUrl: 'url', - endpoints: [{ test: 'foo' }], - msAppId: 'ID', - manifestContent: { - description: 'test skill' + n, - endpoints: [{ test: 'foo' }], - }, -}); - describe('skill dispatcher', () => { let renderedComponent, dispatcher: Dispatcher; beforeEach(() => { @@ -56,7 +42,6 @@ describe('skill dispatcher', () => { const projectId = useRecoilValue(currentProjectIdState); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const onAddSkillDialogComplete = useRecoilValue(onAddSkillDialogCompleteState(projectId)); - const skills: Skill[] = useRecoilValue(skillsState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState(projectId)); const displaySkillManifest = useRecoilValue(displaySkillManifestState(projectId)); @@ -67,7 +52,6 @@ describe('skill dispatcher', () => { projectId, skillManifests, onAddSkillDialogComplete, - skills, settings, showAddSkillDialogModal, displaySkillManifest, @@ -134,61 +118,6 @@ describe('skill dispatcher', () => { ]); }); - it('addsSkill', async () => { - await act(async () => { - dispatcher.addSkill(projectId, makeTestSkill(3)); - }); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(false); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBeUndefined(); - expect(renderedComponent.current.skills).toContainEqual(makeTestSkill(3)); - }); - - it('updateSkill', async () => { - await act(async () => { - dispatcher.updateSkill(projectId, 'id1', { - msAppId: 'test', - manifestUrl: 'test', - endpointUrl: 'test', - name: 'test', - }); - }); - - 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).id); - }); - expect(renderedComponent.current.skills).not.toContain(makeTestSkill(1)); - }); - - it('addSkillDialogBegin', async () => { - await act(async () => { - dispatcher.addSkillDialogBegin(mockDialogComplete, projectId); - }); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(true); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(mockDialogComplete); - }); - - it('addSkillDialogCancel', async () => { - await act(async () => { - dispatcher.addSkillDialogCancel(projectId); - }); - 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/router.tsx b/Composer/packages/client/src/router.tsx index 037bc9eff8..97dd8d17ee 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -25,7 +25,6 @@ const LGPage = React.lazy(() => import('./pages/language-generation/LGPage')); const SettingPage = React.lazy(() => import('./pages/setting/SettingsPage')); const Notifications = React.lazy(() => import('./pages/notifications/Notifications')); const Publish = React.lazy(() => import('./pages/publish/Publish')); -const Skills = React.lazy(() => import('./pages/skills')); const BotCreationFlowRouter = React.lazy(() => import('./components/CreationFlow/CreationFlow')); const FormDialogPage = React.lazy(() => import('./pages/form-dialog/FormDialogPage')); @@ -57,7 +56,6 @@ const Routes = (props) => { - diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index c75839b5e3..6884c04b15 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -28,7 +28,7 @@ import { rateInfoState, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; -import { rootBotProjectIdSelector, skillsStateSelector } from '../recoilModel/selectors'; +import { skillsStateSelector } from '../recoilModel/selectors'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -78,7 +78,6 @@ 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); From a4d5cb61a96ac489832cd44f350a92743f8e78fe Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 20 Oct 2020 12:38:00 -0700 Subject: [PATCH 049/100] Remove skill references Signed-off-by: Srinaath Ravichandran Tested Signed-off-by: Srinaath Ravichandran Remove unused references Signed-off-by: Srinaath Ravichandran --- .../__tests__/components/skill.test.tsx | 4 +- .../TestController/LocalBotRuntime.tsx | 2 +- .../TestController/useLocalBotOperations.ts | 6 +- .../src/pages/knowledge-base/QnAPage.tsx | 3 +- .../src/pages/language-generation/LGPage.tsx | 2 +- .../client/src/pages/setting/SettingsPage.tsx | 1 - .../client/src/pages/skills/index.tsx | 81 ------- .../client/src/pages/skills/skill-list.tsx | 200 ------------------ .../src/pages/skills/skill-settings.tsx | 97 --------- .../client/src/pages/skills/styles.ts | 72 ------- .../dispatchers/__tests__/skill.test.ts | 71 ------- .../src/recoilModel/dispatchers/setting.ts | 23 -- .../persistence/FilePersistence.ts | 4 +- .../src/recoilModel/selectors/skills.ts | 2 +- Composer/packages/client/src/router.tsx | 2 - .../packages/client/src/shell/useShell.ts | 3 +- Composer/packages/lib/shared/src/constant.ts | 2 +- Composer/packages/types/src/shell.ts | 3 +- extensions/localPublish/src/index.ts | 1 - 19 files changed, 14 insertions(+), 565 deletions(-) delete mode 100644 Composer/packages/client/src/pages/skills/index.tsx delete mode 100644 Composer/packages/client/src/pages/skills/skill-list.tsx delete mode 100644 Composer/packages/client/src/pages/skills/skill-settings.tsx delete mode 100644 Composer/packages/client/src/pages/skills/styles.ts diff --git a/Composer/packages/client/__tests__/components/skill.test.tsx b/Composer/packages/client/__tests__/components/skill.test.tsx index 34fefc6adc..df94431cb4 100644 --- a/Composer/packages/client/__tests__/components/skill.test.tsx +++ b/Composer/packages/client/__tests__/components/skill.test.tsx @@ -6,15 +6,13 @@ import { act, fireEvent, getByLabelText, getByTestId, getByText } from '@botfram import { Skill } from '@bfc/shared'; import httpClient from '../../src//utils/httpUtil'; -import SkillList from '../../src/pages/skills/skill-list'; import { renderWithRecoil } from '../testUtils'; import CreateSkillModal, { validateEndpoint, validateManifestUrl, validateName, } from '../../src/components/CreateSkillModal'; -import { currentProjectIdState, settingsState, skillsState } from '../../src/recoilModel'; -import Skills from '../../src/pages/skills'; +import { currentProjectIdState, settingsState } from '../../src/recoilModel'; jest.mock('../../src//utils/httpUtil'); diff --git a/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx index 41393a7001..736814c3c5 100644 --- a/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotRuntime.tsx @@ -47,13 +47,13 @@ export const LocalBotRuntime: React.FC = ({ projectId, dis const botRunIndicatorCallback = useCallback(() => { switch (currentBotStatus) { case BotStatus.connected: - case BotStatus.failed: return ( stopSingleBot(projectId)}> ); case BotStatus.unConnected: + case BotStatus.failed: return ( startSingleBot(projectId)}> diff --git a/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts index c2c7fc45bc..9acf85947a 100644 --- a/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts +++ b/Composer/packages/client/src/components/TestController/useLocalBotOperations.ts @@ -23,11 +23,11 @@ export function useBotOperations(onAllBotsStarted?: (started: boolean) => void) const handleBotStart = async (projectId: string, config: IPublishConfig, botBuildRequired: boolean) => { if (botBuildRequired) { // Default recognizer - const dialogs = builderEssentials.find(({ projectId: currentProjecId }) => projectId === currentProjecId); - if (dialogs) { + const matchedBuilder = builderEssentials.find(({ projectId: currentProjecId }) => projectId === currentProjecId); + if (matchedBuilder?.dialogs) { await botRuntimeOperations?.buildWithDefaultRecognizer(projectId, { recognizers, - dialogs, + dialogs: matchedBuilder.dialogs, config, }); } diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index e3a7bdaa32..f658b0d94f 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -17,6 +17,7 @@ import { Page } from '../../components/Page'; import { dialogsState, qnaFilesState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { CreateQnAModal } from '../../components/QnA'; +import { Dispatcher } from '../../recoilModel/dispatchers'; import TableView from './table-view'; @@ -30,7 +31,7 @@ interface QnAPageProps extends RouteComponentProps<{}> { const QnAPage: React.FC = (props) => { const { dialogId = '', projectId = '' } = props; - const actions = useRecoilValue(dispatcherState); + const actions: Dispatcher = useRecoilValue(dispatcherState); const dialogs = useRecoilValue(dialogsState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); //To do: support other languages diff --git a/Composer/packages/client/src/pages/language-generation/LGPage.tsx b/Composer/packages/client/src/pages/language-generation/LGPage.tsx index 29d89cac44..f9a742092c 100644 --- a/Composer/packages/client/src/pages/language-generation/LGPage.tsx +++ b/Composer/packages/client/src/pages/language-generation/LGPage.tsx @@ -95,7 +95,7 @@ const LGPage: React.FC> = (props: RouteComponen navLinks={navLinks} navRegionName={formatMessage('LG Navigation Pane')} title={formatMessage('Bot Responses')} - toolbarItems={} + toolbarItems={[]} onRenderHeaderContent={onRenderHeaderContent} > }> diff --git a/Composer/packages/client/src/pages/setting/SettingsPage.tsx b/Composer/packages/client/src/pages/setting/SettingsPage.tsx index 38c81cc00b..7302d033b1 100644 --- a/Composer/packages/client/src/pages/setting/SettingsPage.tsx +++ b/Composer/packages/client/src/pages/setting/SettingsPage.tsx @@ -18,7 +18,6 @@ import { settingsState, currentProjectIdState, } from '../../recoilModel'; -import { TestController } from '../../components/TestController/TestController'; import { OpenConfirmModal } from '../../components/Modal/ConfirmDialog'; import { navigateTo } from '../../utils/navigation'; import { Page } from '../../components/Page'; diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx deleted file mode 100644 index 7f2ca848fa..0000000000 --- a/Composer/packages/client/src/pages/skills/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { RouteComponentProps } from '@reach/router'; -import React, { useCallback, useState } from 'react'; -import formatMessage from 'format-message'; -import { useRecoilValue } from 'recoil'; -import { SkillSetting } from '@bfc/shared'; - -import { dispatcherState, settingsState, botDisplayNameState } from '../../recoilModel'; -import { Toolbar, IToolbarItem } from '../../components/Toolbar'; -import { CreateSkillModal } from '../../components/CreateSkillModal'; - -import { ContainerStyle, ContentHeaderStyle, HeaderText } from './styles'; -import SkillSettings from './skill-settings'; -import SkillList from './skill-list'; - -const Skills: React.FC> = (props) => { - const { projectId = '' } = props; - const [showAddSkillDialogModal, setShowAddSkillDialogModal] = useState(false); - - const botName = useRecoilValue(botDisplayNameState(projectId)); - const settings = useRecoilValue(settingsState(projectId)); - const { addSkill, setSettings } = useRecoilValue(dispatcherState); - - const toolbarItems: IToolbarItem[] = [ - { - type: 'action', - text: formatMessage('Connect to a new skill'), - buttonProps: { - iconProps: { - iconName: 'Add', - }, - onClick: () => { - setShowAddSkillDialogModal(true); - }, - }, - align: 'left', - }, - ]; - - const onSubmitForm = useCallback( - (skill: SkillSetting) => { - addSkill(projectId, skill); - setShowAddSkillDialogModal(false); - }, - [projectId] - ); - - const onDismissForm = useCallback(() => { - setShowAddSkillDialogModal(false); - }, []); - - return ( -
- -
-

{formatMessage('Skills')}

-
-
- -
- - {showAddSkillDialogModal && ( - - )} -
- ); -}; - -export default Skills; diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx deleted file mode 100644 index 6fb822fbfd..0000000000 --- a/Composer/packages/client/src/pages/skills/skill-list.tsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { - DetailsList, - DetailsListLayoutMode, - SelectionMode, - CheckboxVisibility, - IColumn, -} from 'office-ui-fabric-react/lib/DetailsList'; -import React, { useState, useCallback, useMemo } from 'react'; -import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; -import { IconButton } from 'office-ui-fabric-react/lib/Button'; -import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; -import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; -import { Stack } from 'office-ui-fabric-react/lib/Stack'; -import { FontSizes } from '@uifabric/fluent-theme'; -import { useRecoilValue } from 'recoil'; -import formatMessage from 'format-message'; -import { Skill } from '@bfc/shared'; - -// import { DisplayManifestModal } from '../../components/Modal/DisplayManifestModal'; -import { dispatcherState, skillsStateSelector } from '../../recoilModel'; - -import { TableView, TableCell } from './styles'; - -const columns: IColumn[] = [ - { - key: 'name', - name: formatMessage('Available Skills'), - fieldName: 'name', - minWidth: 100, - maxWidth: 150, - isResizable: true, - data: 'string', - onRender: ({ skill: { name } }) => { - return
{name}
; - }, - }, - { - key: 'endpointUrl', - name: formatMessage('Skill Endpoint'), - fieldName: 'endpointUrl', - minWidth: 250, - maxWidth: 400, - isResizable: true, - data: 'string', - onRender: ({ skill, onEditSkill }) => { - const { endpoints, endpointUrl: selectedEndpointUrl } = skill; - - const options = (endpoints || []).map(({ name, endpointUrl, msAppId }, key) => ({ - key, - text: name, - data: { - endpointUrl, - msAppId, - }, - selected: endpointUrl === selectedEndpointUrl, - })); - - const handleChange = (_, option?: IDropdownOption) => { - if (option) { - onEditSkill({ ...skill, ...option.data }); - } - }; - - return ; - }, - }, - { - key: 'description', - name: formatMessage('Description'), - fieldName: 'description', - minWidth: 200, - maxWidth: 400, - isResizable: true, - data: 'string', - onRender: ({ skill: { description } }) => { - return
{description}
; - }, - }, - { - key: 'buttons', - name: '', - minWidth: 120, - maxWidth: 120, - fieldName: 'buttons', - data: 'string', - onRender: ({ onDelete, onViewManifest }) => { - return ( -
- - onDelete()} - /> - onViewManifest()} - /> - -
- ); - }, - }, -]; - -interface SkillListProps { - projectId: string; -} - -const SkillList: React.FC = ({ projectId }) => { - const { removeSkillFromBotProject, addRemoteSkillToBotProject } = useRecoilValue(dispatcherState); - const skills = useRecoilValue(skillsStateSelector); - - const [selectedSkillUrl, setSelectedSkillUrl] = useState(null); - - const handleViewManifest = (item) => { - if (item && item.name && item.content) { - setSelectedSkillUrl(item.manifestUrl); - } - }; - - const handleEditSkill = (projectId, skillId) => (skillData) => {}; - - const generateSkillsList = (skills: Record) => { - const skillsList: any[] = []; - for (const skillName in skills) { - const currentSkill = skills[skillName]; - skillsList.push({ - skill: { - name: currentSkill.name, - description: currentSkill.description, - onDelete: () => removeSkillFromBotProject(currentSkill.id), - onViewManifest: () => handleViewManifest(currentSkill.id), - onEditSkill: handleEditSkill(projectId, currentSkill.id), - }, - }); - } - return skillsList; - }; - - const items = useMemo(() => generateSkillsList(skills), [skills, projectId]); - - const onDismissManifest = () => { - setSelectedSkillUrl(null); - }; - - const onRenderDetailsHeader = useCallback((props, defaultRender) => { - return ( -
- - {defaultRender({ - ...props, - onRenderColumnHeaderTooltip: (tooltipHostProps) => , - })} - -
- ); - }, []); - - return ( - -
- - - -
- {/* */} -
- ); -}; - -export default SkillList; diff --git a/Composer/packages/client/src/pages/skills/skill-settings.tsx b/Composer/packages/client/src/pages/skills/skill-settings.tsx deleted file mode 100644 index cb14016c1a..0000000000 --- a/Composer/packages/client/src/pages/skills/skill-settings.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React, { useState, useEffect } from 'react'; -import formatMessage from 'format-message'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import { DialogSetting } from '@bfc/shared'; - -import { FormFieldAlignHorizontalBotSettings } from './styles'; - -export interface ISkillFormProps { - botId?: string; - skillHostEndpoint?: string; - botPassword?: string; - setSettings: (projectId: string, settings: DialogSetting) => Promise; - projectId: string; - botName: string; - settings: DialogSetting; -} - -const SkillSettings: React.FC = (props) => { - const [skillSettings, setSkillSettings] = useState({ - botId: props.botId, - skillHostEndpoint: props.skillHostEndpoint, - botPassword: props.botPassword, - }); - useEffect(() => { - setSkillSettings({ - botId: props.botId, - skillHostEndpoint: props.skillHostEndpoint, - botPassword: props.botPassword, - }); - }, [props.botId, props.botPassword, props.skillHostEndpoint]); - - const handleFieldChange = (event) => { - const localSettings = { - ...skillSettings, - [event.target.id]: event.target.value, - }; - - // auto fill `botId` with `MicrosoftAppId` - if (event.target.id === 'MicrosoftAppId') { - localSettings.botId = event.target.value; - } - - setSkillSettings({ ...localSettings }); - props.setSettings(props.projectId, { ...props.settings, ...localSettings }); - }; - - return ( -
-
- -
-
- -
-
- -
-
- ); -}; - -export default SkillSettings; diff --git a/Composer/packages/client/src/pages/skills/styles.ts b/Composer/packages/client/src/pages/skills/styles.ts deleted file mode 100644 index 9221fa0a08..0000000000 --- a/Composer/packages/client/src/pages/skills/styles.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { FontWeights, FontSizes } from 'office-ui-fabric-react/lib/Styling'; -import { css } from '@emotion/core'; - -export const ContainerStyle = css` - display: flex; - flex-direction: column; - height: 100%; -`; - -export const ContentHeaderStyle = css` - padding: 5px 20px; - height: 60px; - display: flex; - flex-shrink: 0; - justify-content: space-between; - align-items: center; -`; - -export const HeaderText = css` - font-size: ${FontSizes.xLarge}; - font-weight: ${FontWeights.semibold}; -`; - -export const ContentStyle = css` - margin-left: 2px; - display: flex; - border-top: 1px solid #dddddd; - flex: 1; - position: relative; - nav { - ul { - margin-top: 0px; - } - } -`; - -export const TableView = css` - flex: 4; - margin: 20px; - position: relative; - overflow: visible; - fontsize: 16px; -`; - -export const TableCell = css` - white-space: pre-wrap; - font-size: 14px; - textarea, - input { - border: 1px solid #dddddd; - } -`; - -export const FormFieldAlignHorizontalBotSettings = css` - max-width: 1500px; - margin-top: 20px; - align-items: flex-start; - display: flex; - flex-wrap: wrap; - flex-direction: row; - padding-bottom: 15px; -`; - -export const ManifestModalHeaderStyle = css` - margin: 14px 0 0 16px; - font-size: 20px; - font-weight: bolder; - align-items: left; -`; 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 8858f0173d..13d4928b19 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts @@ -33,20 +33,6 @@ jest.mock('../../../utils/httpUtil', () => { const mockDialogComplete = jest.fn(); const projectId = '42345.23432'; -const makeTestSkill: (number) => Skill = (n) => ({ - id: 'id' + n, - manifestUrl: 'url' + n, - name: 'skill' + n, - description: 'test skill' + n, - endpointUrl: 'url', - endpoints: [{ test: 'foo' }], - msAppId: 'ID', - manifestContent: { - description: 'test skill' + n, - endpoints: [{ test: 'foo' }], - }, -}); - describe('skill dispatcher', () => { let renderedComponent, dispatcher: Dispatcher; beforeEach(() => { @@ -56,7 +42,6 @@ describe('skill dispatcher', () => { const projectId = useRecoilValue(currentProjectIdState); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const onAddSkillDialogComplete = useRecoilValue(onAddSkillDialogCompleteState(projectId)); - const skills: Skill[] = useRecoilValue(skillsState(projectId)); const settings = useRecoilValue(settingsState(projectId)); const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState(projectId)); const displaySkillManifest = useRecoilValue(displaySkillManifestState(projectId)); @@ -67,7 +52,6 @@ describe('skill dispatcher', () => { projectId, skillManifests, onAddSkillDialogComplete, - skills, settings, showAddSkillDialogModal, displaySkillManifest, @@ -134,61 +118,6 @@ describe('skill dispatcher', () => { ]); }); - it('addsSkill', async () => { - await act(async () => { - dispatcher.addSkill(projectId, makeTestSkill(3)); - }); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(false); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBeUndefined(); - expect(renderedComponent.current.skills).toContainEqual(makeTestSkill(3)); - }); - - it('updateSkill', async () => { - await act(async () => { - dispatcher.updateSkill(projectId, 'id1', { - msAppId: 'test', - manifestUrl: 'test', - endpointUrl: 'test', - name: 'test', - }); - }); - - 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).id); - }); - expect(renderedComponent.current.skills).not.toContain(makeTestSkill(1)); - }); - - it('addSkillDialogBegin', async () => { - await act(async () => { - dispatcher.addSkillDialogBegin(mockDialogComplete, projectId); - }); - expect(renderedComponent.current.showAddSkillDialogModal).toBe(true); - expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(mockDialogComplete); - }); - - it('addSkillDialogCancel', async () => { - await act(async () => { - dispatcher.addSkillDialogCancel(projectId); - }); - 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/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts index 4a5c46ee80..adebbffb71 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts @@ -98,29 +98,6 @@ export const settingsDispatcher = () => { } } ); - - const setQnASettings = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (projectId: string, subscriptionKey: string) => { - const { set } = callbackHelpers; - try { - const response = await httpClient.post(`/projects/${projectId}/qnaSettings/set`, { - projectId, - subscriptionKey, - }); - settingStorage.setField(projectId, 'qna.endpointKey', response.data); - set(settingsState(projectId), (currentValue) => ({ - ...currentValue, - qna: { - ...currentValue.qna, - endpointKey: response.data, - }, - })); - } catch (err) { - setError(callbackHelpers, err); - } - } - ); - return { setSettings, setRuntimeSettings, diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts index 378842a540..8fb543fa31 100644 --- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts +++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts @@ -7,13 +7,13 @@ import { DialogInfo, DialogSchemaFile, DialogSetting, - SkillManifest, BotAssets, BotProjectFile, LuFile, LgFile, QnAFile, FormDialogSchema, + SkillManifestFile, } from '@bfc/shared'; import keys from 'lodash/keys'; @@ -189,7 +189,7 @@ class FilePersistence { return changes; } - private getSkillManifestsChanges(current: SkillManifest[], previous: SkillManifest[]) { + private getSkillManifestsChanges(current: SkillManifestFile[], previous: SkillManifestFile[]) { const changeItems = this.getDifferenceItems(current, previous); const changes = this.getFileChanges(FileExtensions.Manifest, changeItems); return changes; diff --git a/Composer/packages/client/src/recoilModel/selectors/skills.ts b/Composer/packages/client/src/recoilModel/selectors/skills.ts index abc3c8de86..2c682cffe1 100644 --- a/Composer/packages/client/src/recoilModel/selectors/skills.ts +++ b/Composer/packages/client/src/recoilModel/selectors/skills.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Skill, Manifest } from '@bfc/types'; +import { Manifest, Skill } from '@bfc/shared'; import { selector } from 'recoil'; import { diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index 037bc9eff8..97dd8d17ee 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -25,7 +25,6 @@ const LGPage = React.lazy(() => import('./pages/language-generation/LGPage')); const SettingPage = React.lazy(() => import('./pages/setting/SettingsPage')); const Notifications = React.lazy(() => import('./pages/notifications/Notifications')); const Publish = React.lazy(() => import('./pages/publish/Publish')); -const Skills = React.lazy(() => import('./pages/skills')); const BotCreationFlowRouter = React.lazy(() => import('./components/CreationFlow/CreationFlow')); const FormDialogPage = React.lazy(() => import('./pages/form-dialog/FormDialogPage')); @@ -57,7 +56,6 @@ const Routes = (props) => { - diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index c75839b5e3..6884c04b15 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -28,7 +28,7 @@ import { rateInfoState, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; -import { rootBotProjectIdSelector, skillsStateSelector } from '../recoilModel/selectors'; +import { skillsStateSelector } from '../recoilModel/selectors'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -78,7 +78,6 @@ 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); diff --git a/Composer/packages/lib/shared/src/constant.ts b/Composer/packages/lib/shared/src/constant.ts index ac883acaa4..d17fa58b3d 100644 --- a/Composer/packages/lib/shared/src/constant.ts +++ b/Composer/packages/lib/shared/src/constant.ts @@ -18,7 +18,7 @@ export const FieldNames = { }; export const defaultPublishConfig = { name: 'default', - type: 'localpublish', + type: 'localPublisher', configuration: JSON.stringify({}), }; diff --git a/Composer/packages/types/src/shell.ts b/Composer/packages/types/src/shell.ts index 2ccde7221d..caf5cacafb 100644 --- a/Composer/packages/types/src/shell.ts +++ b/Composer/packages/types/src/shell.ts @@ -97,7 +97,6 @@ export type ProjectContextApi = { displayManifestModal: (manifestId: string) => void; updateDialogSchema: (_: DialogSchemaFile) => Promise; createTrigger: (id: string, formData, autoSelected?: boolean) => void; - updateSkillSetting: (skillId: string, skillsData: SkillSetting) => Promise; updateFlowZoomRate: (currentRate: number) => void; updateSkill: (skillId: string, skillsData: { skill: Skill; selectedEndpointIndex: number }) => Promise; }; @@ -111,7 +110,7 @@ export type ProjectContext = { luFiles: LuFile[]; luFeatures: ILUFeaturesConfig; qnaFiles: QnAFile[]; - skills: any[]; + skills: Record; skillsSettings: Record; schemas: BotSchemas; }; diff --git a/extensions/localPublish/src/index.ts b/extensions/localPublish/src/index.ts index fdc6788e6a..7df7597287 100644 --- a/extensions/localPublish/src/index.ts +++ b/extensions/localPublish/src/index.ts @@ -295,7 +295,6 @@ class LocalPublisher implements PublishPlugin { } if (!port) { port = await getPort({ port: this.getAvailablePorts() }); - // port = await portfinder.getPortPromise({ port: 3979, stopPort: 5000 }); } // if not using custom runtime, update assets in tmp older From 5c281914e2a24aabd3b52d67b9d4cb0385f0fbbe Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 20 Oct 2020 14:50:27 -0700 Subject: [PATCH 050/100] Update references Signed-off-by: Srinaath Ravichandran --- .../src/pages/design/exportSkillModal/constants.tsx | 10 +++++----- .../recoilModel/dispatchers/__tests__/project.test.tsx | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx index c99ddd7e7e..1fa550ebb9 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx @@ -4,7 +4,7 @@ import formatMessage from 'format-message'; import { JSONSchema7 } from '@bfc/extension-client'; import { resolveRef } from '@bfc/adaptive-form'; -import { SkillManifest } from '@bfc/shared'; +import { SkillManifestFile } from '@bfc/shared'; import startCase from 'lodash/startCase'; import { SDKKinds } from '@bfc/shared'; @@ -88,14 +88,14 @@ export interface ContentProps { completeStep: () => void; errors: { [key: string]: any }; editJson: () => void; - manifest: Partial; + manifest: Partial; setErrors: (errors: { [key: string]: any }) => void; setSchema: (_: JSONSchema7) => void; setSelectedDialogs: (dialogs: any[]) => void; setSelectedTriggers: (selectedTriggers: any[]) => void; - setSkillManifest: (_: Partial) => void; + setSkillManifest: (_: Partial) => void; schema: JSONSchema7; - skillManifests: SkillManifest[]; + skillManifests: SkillManifestFile[]; value: { [key: string]: any }; onChange: (_: any) => void; projectId: string; @@ -113,7 +113,7 @@ interface ValidationDetails { editingId?: string; id?: string; schema: any; - skillManifests: SkillManifest[]; + skillManifests: SkillManifestFile[]; } interface EditorStep { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index 777c6faa79..4374cb41e7 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -272,7 +272,6 @@ describe('Project dispatcher', () => { expect(renderedComponent.current.lgFiles.length).toBe(0); expect(renderedComponent.current.luFiles.length).toBe(0); expect(renderedComponent.current.botEnvironment).toBe('production'); - expect(renderedComponent.current.skills.length).toBe(0); expect(renderedComponent.current.botOpening).toBeFalsy(); expect(renderedComponent.current.schemas.sdk).toBeUndefined(); expect(renderedComponent.current.schemas.default).toBeUndefined(); @@ -281,7 +280,7 @@ describe('Project dispatcher', () => { it('should set bot status', async () => { await act(async () => { - await dispatcher.setBotStatus(BotStatus.pending, projectId); + await dispatcher.setBotStatus(projectId, BotStatus.pending); }); expect(renderedComponent.current.botStatus).toEqual(BotStatus.pending); @@ -391,7 +390,6 @@ describe('Project dispatcher', () => { await act(async () => { await dispatcher.addRemoteSkillToBotProject( 'https://test-dev.azurewebsites.net/manifests/onenote-2-1-preview-1-manifest.json', - 'one-note', 'remote' ); }); From 0ba924bc7e76cf61cdb20aab152455b4a5290387 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 20 Oct 2020 17:30:53 -0700 Subject: [PATCH 051/100] Updated runtime experience Signed-off-by: Srinaath Ravichandran --- .../src/components/CreateSkillModal.tsx | 2 +- .../components/ProjectTree/ProjectTree.tsx | 28 +- .../LocalBotStatusIndicator.tsx | 13 +- .../TestController/TestController.tsx | 482 +++++++++--------- .../dispatchers/__tests__/project.test.tsx | 3 - .../dispatchers/__tests__/setting.test.tsx | 4 +- .../src/recoilModel/dispatchers/publisher.ts | 7 +- extensions/localPublish/src/index.ts | 9 +- 8 files changed, 277 insertions(+), 271 deletions(-) diff --git a/Composer/packages/client/src/components/CreateSkillModal.tsx b/Composer/packages/client/src/components/CreateSkillModal.tsx index 2415ff812a..45fded386a 100644 --- a/Composer/packages/client/src/components/CreateSkillModal.tsx +++ b/Composer/packages/client/src/components/CreateSkillModal.tsx @@ -157,7 +157,7 @@ export const CreateSkillModal: React.FC = ({ projectId, o }; const handleManifestUrlChange = (_, manifestUrl = '') => { - const { msAppId, endpointUrl, ...rest } = formData; + const { msAppId, endpoint, ...rest } = formData; setValidationState((validationState) => ({ ...validationState, manifestUrl: ValidationState.NotValidated, diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index fe9a06b5dc..a079dfb6b4 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -151,7 +151,9 @@ export const ProjectTree: React.FC = ({ } const dialogHasWarnings = (dialog: DialogInfo) => { - notificationMap[currentProjectId][dialog.id].some((diag) => diag.severity === DiagnosticSeverity.Warning); + if (notificationMap[currentProjectId][dialog.id]) { + return notificationMap[currentProjectId][dialog.id].some((diag) => diag.severity === DiagnosticSeverity.Warning); + } }; const botHasWarnings = (bot: BotInProject) => { @@ -159,7 +161,9 @@ export const ProjectTree: React.FC = ({ }; const dialogHasErrors = (dialog: DialogInfo) => { - notificationMap[currentProjectId][dialog.id].some((diag) => diag.severity === DiagnosticSeverity.Error); + if (notificationMap[currentProjectId][dialog.id]) { + notificationMap[currentProjectId][dialog.id].some((diag) => diag.severity === DiagnosticSeverity.Error); + } }; const botHasErrors = (bot: BotInProject) => { @@ -211,14 +215,18 @@ export const ProjectTree: React.FC = ({ }; const renderDialogHeader = (skillId: string, dialog: DialogInfo) => { - const warningContent = notificationMap[currentProjectId][dialog.id] - .filter((diag) => diag.severity === DiagnosticSeverity.Warning) - .map((diag) => diag.message) - .join(','); - const errorContent = notificationMap[currentProjectId][dialog.id] - .filter((diag) => diag.severity === DiagnosticSeverity.Error) - .map((diag) => diag.message) - .join(','); + let warningContent, errorContent; + if (notificationMap[currentProjectId][dialog.id]) { + warningContent = notificationMap[currentProjectId][dialog.id] + .filter((diag) => diag.severity === DiagnosticSeverity.Warning) + .map((diag) => diag.message) + .join(','); + + errorContent = notificationMap[currentProjectId][dialog.id] + .filter((diag) => diag.severity === DiagnosticSeverity.Error) + .map((diag) => diag.message) + .join(','); + } const link: TreeLink = { dialogName: dialog.id, diff --git a/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx index 67d9724569..e60536df67 100644 --- a/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx +++ b/Composer/packages/client/src/components/TestController/LocalBotStatusIndicator.tsx @@ -43,7 +43,7 @@ export const LocalBotStatusIndicator: React.FC = ( const botActionRef = useRef(null); const botLoadErrorMsg = useRecoilValue(botRuntimeErrorState(projectId)); const [calloutVisible, setErrorCallout] = useState(false); - const { startSingleBot } = useBotOperations(undefined); + const { startSingleBot, stopSingleBot } = useBotOperations(); useInterval( () => { @@ -65,6 +65,7 @@ export const LocalBotStatusIndicator: React.FC = ( case BotStatus.failed: setBotStatusText(BotStatusesCopy[BotStatus.failed]); setIntervalRunning(false); + stopSingleBot(projectId); break; case BotStatus.published: setBotStatusText(BotStatusesCopy[BotStatus.published]); @@ -76,11 +77,15 @@ export const LocalBotStatusIndicator: React.FC = ( setIntervalRunning(true); break; - case BotStatus.connected: - setIntervalRunning(false); + case BotStatus.connected: { + // Runtime errors aren't surface immediately. Stop the interval after one more ping setBotStatusText(BotStatusesCopy[BotStatus.connected]); + setIntervalRunning(false); + setTimeout(() => { + getPublishStatus(projectId, defaultPublishConfig); + }, 3000); break; - + } case BotStatus.publishing: setBotStatusText(BotStatusesCopy[BotStatus.publishing]); break; diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index 2491f44af3..5d8c10d5a7 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -1,268 +1,268 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. -/** @jsx jsx */ +// /** @jsx jsx */ -import React, { useState, useRef, Fragment, useEffect, useCallback } from 'react'; -import { jsx, css } from '@emotion/core'; -import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; -import formatMessage from 'format-message'; -import { useRecoilValue } from 'recoil'; -import { IConfig, IPublishConfig, defaultPublishConfig } from '@bfc/shared'; -import { useRecognizerConfig } from '@bfc/extension-client'; +// import React, { useState, useRef, Fragment, useEffect, useCallback } from 'react'; +// import { jsx, css } from '@emotion/core'; +// import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; +// import formatMessage from 'format-message'; +// import { useRecoilValue } from 'recoil'; +// import { IConfig, IPublishConfig, defaultPublishConfig } from '@bfc/shared'; +// import { useRecognizerConfig } from '@bfc/extension-client'; -import { - botEndpointsState, - dispatcherState, - validateDialogSelectorFamily, - botStatusState, - botDisplayNameState, - luFilesState, - qnaFilesState, - settingsState, - botRuntimeErrorState, -} from '../../recoilModel'; -import settingsStorage from '../../utils/dialogSettingStorage'; -import { BotStatus } from '../../constants'; -import { isAbsHosted } from '../../utils/envUtil'; -import useNotifications from '../../pages/notifications/useNotifications'; -import { navigateTo, openInEmulator } from '../../utils/navigation'; +// import { +// botEndpointsState, +// dispatcherState, +// validateDialogSelectorFamily, +// botStatusState, +// botDisplayNameState, +// luFilesState, +// qnaFilesState, +// settingsState, +// botRuntimeErrorState, +// } from '../../recoilModel'; +// import settingsStorage from '../../utils/dialogSettingStorage'; +// import { BotStatus } from '../../constants'; +// import { isAbsHosted } from '../../utils/envUtil'; +// import useNotifications from '../../pages/notifications/useNotifications'; +// import { navigateTo, openInEmulator } from '../../utils/navigation'; -import { isBuildConfigComplete, needsBuild } from './../../utils/buildUtil'; -import { PublishDialog } from './publishDialog'; -import { ErrorCallout } from './errorCallout'; -import { EmulatorOpenButton } from './emulatorOpenButton'; -import { Loading } from './loading'; -import { ErrorInfo } from './errorInfo'; -import { WarningInfo } from './warningInfo'; +// import { isBuildConfigComplete, needsBuild } from './../../utils/buildUtil'; +// import { PublishDialog } from './publishDialog'; +// import { ErrorCallout } from './errorCallout'; +// import { EmulatorOpenButton } from './emulatorOpenButton'; +// import { Loading } from './loading'; +// import { ErrorInfo } from './errorInfo'; +// import { WarningInfo } from './warningInfo'; -// -------------------- Styles -------------------- // +// // -------------------- Styles -------------------- // -export const bot = css` - display: flex; - align-items: center; - position: relative; - height: 100%; -`; +// export const bot = css` +// display: flex; +// align-items: center; +// position: relative; +// height: 100%; +// `; -export const botButton = css` - margin-left: 5px; -`; +// export const botButton = css` +// margin-left: 5px; +// `; -let botStatusInterval: NodeJS.Timeout | undefined = undefined; +// let botStatusInterval: NodeJS.Timeout | undefined = undefined; -// -------------------- TestController -------------------- // -const POLLING_INTERVAL = 2500; -export const TestController: React.FC<{ projectId: string }> = (props) => { - const { projectId = '' } = props; - const [modalOpen, setModalOpen] = useState(false); - const [calloutVisible, setCalloutVisible] = useState(false); +// // -------------------- TestController -------------------- // +// const POLLING_INTERVAL = 2500; +// export const TestController: React.FC<{ projectId: string }> = (props) => { +// const { projectId = '' } = props; +// const [modalOpen, setModalOpen] = useState(false); +// const [calloutVisible, setCalloutVisible] = useState(false); - const botActionRef = useRef(null); - const notifications = useNotifications(projectId); +// const botActionRef = useRef(null); +// const notifications = useNotifications(projectId); - const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); - const botStatus = useRecoilValue(botStatusState(projectId)); - const botName = useRecoilValue(botDisplayNameState(projectId)); - const luFiles = useRecoilValue(luFilesState(projectId)); - const settings = useRecoilValue(settingsState(projectId)); - const qnaFiles = useRecoilValue(qnaFilesState(projectId)); - const botLoadErrorMsg = useRecoilValue(botRuntimeErrorState(projectId)); - const { recognizers } = useRecognizerConfig(); +// const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); +// const botStatus = useRecoilValue(botStatusState(projectId)); +// const botName = useRecoilValue(botDisplayNameState(projectId)); +// const luFiles = useRecoilValue(luFilesState(projectId)); +// const settings = useRecoilValue(settingsState(projectId)); +// const qnaFiles = useRecoilValue(qnaFilesState(projectId)); +// const botLoadErrorMsg = useRecoilValue(botRuntimeErrorState(projectId)); +// const { recognizers } = useRecognizerConfig(); - const botEndpoints = useRecoilValue(botEndpointsState); - const { - publishToTarget, - onboardingAddCoachMarkRef, - build, - getPublishStatus, - setBotStatus, - setSettings, - setQnASettings, - } = useRecoilValue(dispatcherState); - const connected = botStatus === BotStatus.connected; - const publishing = botStatus === BotStatus.publishing; - const reloading = botStatus === BotStatus.reloading; - const addRef = useCallback((startBot) => onboardingAddCoachMarkRef({ startBot }), []); - const errorLength = notifications.filter((n) => n.severity === 'Error').length; - const showError = errorLength > 0; - const publishDialogConfig = { subscriptionKey: settings.qna?.subscriptionKey, ...settings.luis } as IConfig; - const warningLength = notifications.filter((n) => n.severity === 'Warning').length; - const showWarning = !showError && warningLength > 0; +// const botEndpoints = useRecoilValue(botEndpointsState); +// const { +// publishToTarget, +// onboardingAddCoachMarkRef, +// build, +// getPublishStatus, +// setBotStatus, +// setSettings, +// setQnASettings, +// } = useRecoilValue(dispatcherState); +// const connected = botStatus === BotStatus.connected; +// const publishing = botStatus === BotStatus.publishing; +// const reloading = botStatus === BotStatus.reloading; +// const addRef = useCallback((startBot) => onboardingAddCoachMarkRef({ startBot }), []); +// const errorLength = notifications.filter((n) => n.severity === 'Error').length; +// const showError = errorLength > 0; +// const publishDialogConfig = { subscriptionKey: settings.qna?.subscriptionKey, ...settings.luis } as IConfig; +// const warningLength = notifications.filter((n) => n.severity === 'Warning').length; +// const showWarning = !showError && warningLength > 0; - useEffect(() => { - if (projectId) { - getPublishStatus(projectId, defaultPublishConfig); - } - }, [projectId]); +// useEffect(() => { +// if (projectId) { +// getPublishStatus(projectId, defaultPublishConfig); +// } +// }, [projectId]); - // useEffect(() => { - // switch (botStatus) { - // case BotStatus.failed: - // openCallout(); - // stopPollingRuntime(); - // setBotStatus(projectId, BotStatus.pending); - // break; - // case BotStatus.published: - // stopPollingRuntime(); - // handleLoadBot(); - // break; - // case BotStatus.reloading: - // startPollingRuntime(); - // break; - // default: - // case BotStatus.connected: - // stopPollingRuntime(); - // break; - // } - // return () => { - // stopPollingRuntime(); - // return; - // }; - // }, [botStatus]); +// // useEffect(() => { +// // switch (botStatus) { +// // case BotStatus.failed: +// // openCallout(); +// // stopPollingRuntime(); +// // setBotStatus(projectId, BotStatus.pending); +// // break; +// // case BotStatus.published: +// // stopPollingRuntime(); +// // handleLoadBot(); +// // break; +// // case BotStatus.reloading: +// // startPollingRuntime(); +// // break; +// // default: +// // case BotStatus.connected: +// // stopPollingRuntime(); +// // break; +// // } +// // return () => { +// // stopPollingRuntime(); +// // return; +// // }; +// // }, [botStatus]); - function dismissDialog() { - setModalOpen(false); - } +// function dismissDialog() { +// setModalOpen(false); +// } - function openDialog() { - setModalOpen(true); - } +// function openDialog() { +// setModalOpen(true); +// } - function dismissCallout() { - if (calloutVisible) setCalloutVisible(false); - } +// function dismissCallout() { +// if (calloutVisible) setCalloutVisible(false); +// } - function openCallout() { - setCalloutVisible(true); - } +// function openCallout() { +// setCalloutVisible(true); +// } - function startPollingRuntime() { - if (!botStatusInterval) { - const cancelInterval = setInterval(() => { - // get publish status - getPublishStatus(projectId, defaultPublishConfig); - }, POLLING_INTERVAL); - botStatusInterval = cancelInterval; - } - } +// function startPollingRuntime() { +// if (!botStatusInterval) { +// const cancelInterval = setInterval(() => { +// // get publish status +// getPublishStatus(projectId, defaultPublishConfig); +// }, POLLING_INTERVAL); +// botStatusInterval = cancelInterval; +// } +// } - function stopPollingRuntime() { - if (botStatusInterval) { - clearInterval(botStatusInterval); - botStatusInterval = undefined; - } - } +// function stopPollingRuntime() { +// if (botStatusInterval) { +// clearInterval(botStatusInterval); +// botStatusInterval = undefined; +// } +// } - async function handleBuild(config: IPublishConfig) { - setBotStatus(projectId, BotStatus.publishing); - dismissDialog(); - const { luis, qna } = config; - const recognizerTypes = dialogs.reduce((result, file) => { - const recognizer = recognizers.filter((r) => r.isSelected && r.isSelected(file.content.recognizer)); - result[file.id] = recognizer[0]?.id || ''; - return result; - }, {}); +// async function handleBuild(config: IPublishConfig) { +// setBotStatus(projectId, BotStatus.publishing); +// dismissDialog(); +// const { luis, qna } = config; +// const recognizerTypes = dialogs.reduce((result, file) => { +// const recognizer = recognizers.filter((r) => r.isSelected && r.isSelected(file.content.recognizer)); +// result[file.id] = recognizer[0]?.id || ''; +// return result; +// }, {}); - await setSettings(projectId, { - ...settings, - luis: luis, - qna: Object.assign({}, settings.qna, qna), - }); - await build(projectId, luis, qna, recognizerTypes); - } +// await setSettings(projectId, { +// ...settings, +// luis: luis, +// qna: Object.assign({}, settings.qna, qna), +// }); +// await build(projectId, luis, qna, recognizerTypes); +// } - async function handleLoadBot() { - setBotStatus(projectId, BotStatus.reloading); - if (settings.qna && settings.qna.subscriptionKey) { - await setQnASettings(projectId, settings.qna.subscriptionKey); - } - const sensitiveSettings = settingsStorage.get(projectId); - await publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); - } +// async function handleLoadBot() { +// setBotStatus(projectId, BotStatus.reloading); +// if (settings.qna && settings.qna.subscriptionKey) { +// await setQnASettings(projectId, settings.qna.subscriptionKey); +// } +// const sensitiveSettings = settingsStorage.get(projectId); +// await publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); +// } - async function handleStart() { - dismissCallout(); - const config = Object.assign( - {}, - { - luis: settings.luis, - qna: settings.qna, - } - ); - if (!isAbsHosted() && needsBuild(dialogs)) { - if ( - botStatus === BotStatus.failed || - botStatus === BotStatus.pending || - !isBuildConfigComplete(config, dialogs, luFiles, qnaFiles) - ) { - openDialog(); - } else { - await handleBuild(config); - } - } else { - await handleLoadBot(); - } - } +// async function handleStart() { +// dismissCallout(); +// const config = Object.assign( +// {}, +// { +// luis: settings.luis, +// qna: settings.qna, +// } +// ); +// if (!isAbsHosted() && needsBuild(dialogs)) { +// if ( +// botStatus === BotStatus.failed || +// botStatus === BotStatus.pending || +// !isBuildConfigComplete(config, dialogs, luFiles, qnaFiles) +// ) { +// openDialog(); +// } else { +// await handleBuild(config); +// } +// } else { +// await handleLoadBot(); +// } +// } - function handleErrorButtonClick() { - navigateTo(`/bot/${projectId}/notifications`); - } +// function handleErrorButtonClick() { +// navigateTo(`/bot/${projectId}/notifications`); +// } - async function handleOpenEmulator() { - return Promise.resolve( - openInEmulator( - botEndpoints[projectId] || 'http://localhost:3979/api/messages', - settings.MicrosoftAppId && settings.MicrosoftAppPassword - ? { MicrosoftAppId: settings.MicrosoftAppId, MicrosoftAppPassword: settings.MicrosoftAppPassword } - : { MicrosoftAppPassword: '', MicrosoftAppId: '' } - ) - ); - } +// async function handleOpenEmulator() { +// return Promise.resolve( +// openInEmulator( +// botEndpoints[projectId] || 'http://localhost:3979/api/messages', +// settings.MicrosoftAppId && settings.MicrosoftAppPassword +// ? { MicrosoftAppId: settings.MicrosoftAppId, MicrosoftAppPassword: settings.MicrosoftAppPassword } +// : { MicrosoftAppPassword: '', MicrosoftAppId: '' } +// ) +// ); +// } - return ( - -
-