diff --git a/Composer/packages/client/src/shell/actionApi.ts b/Composer/packages/client/src/shell/actionApi.ts index 9dfb791e76..e4e3741848 100644 --- a/Composer/packages/client/src/shell/actionApi.ts +++ b/Composer/packages/client/src/shell/actionApi.ts @@ -5,12 +5,10 @@ import { deepCopyActions, deleteActions as destructActions, FieldProcessorAsync, - walkAdaptiveActionList, - LgType, - LgMetaData, LgTemplateRef, - LuType, LuMetaData, + LuType, + walkAdaptiveActionList, } from '@bfc/shared'; import { LuIntentSection, MicrosoftIDialog } from '@botframework-composer/types'; @@ -18,6 +16,7 @@ import TelemetryClient from '../telemetry/TelemetryClient'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; +import { deserializeLgTemplate, serializeLgTemplate } from './utils'; export const useActionApi = (projectId: string) => { const { getLgTemplates, removeLgTemplates, addLgTemplate } = useLgApi(projectId); @@ -37,20 +36,16 @@ export const useActionApi = (projectId: string) => { const createLgTemplate = async ( lgFileId: string, + toId: string, lgText: string, - hostActionId: string, hostActionData: MicrosoftIDialog, hostFieldName: string ): Promise => { if (!lgText) return ''; - const newLgType = new LgType(hostActionData.$kind, hostFieldName).toString(); - const newLgTemplateName = new LgMetaData(newLgType, hostActionId).toString(); - const newLgTemplateRefStr = new LgTemplateRef(newLgTemplateName).toString(); - await addLgTemplate(lgFileId, newLgTemplateName, lgText); - return newLgTemplateRefStr; + return await deserializeLgTemplate(lgFileId, toId, lgText, hostActionData, hostFieldName, addLgTemplate); }; - const readLgTemplate = (lgText: string) => { + const readLgTemplate = (lgText: string, fromId: string) => { if (!lgText) return ''; const inputLgRef = LgTemplateRef.parse(lgText); @@ -59,12 +54,13 @@ export const useActionApi = (projectId: string) => { const lgTemplates = getLgTemplates(inputLgRef.name); if (!Array.isArray(lgTemplates) || !lgTemplates.length) return lgText; - const targetTemplate = lgTemplates.find((x) => x.name === inputLgRef.name); - return targetTemplate ? targetTemplate.body : lgText; + const serializedLg = serializeLgTemplate(inputLgRef.name, fromId, lgText, lgTemplates); + + return serializedLg; }; const createLuIntent = async ( - luFildId: string, + luFileId: string, intent: LuIntentSection | undefined, hostResourceId: string, hostResourceData: MicrosoftIDialog @@ -74,7 +70,7 @@ export const useActionApi = (projectId: string) => { const newLuIntentType = new LuType(hostResourceData.$kind).toString(); const newLuIntentName = new LuMetaData(newLuIntentType, hostResourceId).toString(); const newLuIntent: LuIntentSection = { ...intent, Name: newLuIntentName }; - await addLuIntent(luFildId, newLuIntentName, newLuIntent); + await addLuIntent(luFileId, newLuIntentName, newLuIntent); return newLuIntentName; }; @@ -90,7 +86,7 @@ export const useActionApi = (projectId: string) => { }); // '- hi' -> 'SendActivity_1234' const referenceLgText: FieldProcessorAsync = async (fromId, fromAction, toId, toAction, lgFieldName) => - createLgTemplate(dialogId, fromAction[lgFieldName] as string, toId, toAction, lgFieldName); + createLgTemplate(dialogId, toId, fromAction[lgFieldName] as string, toAction, lgFieldName); // LuIntentSection -> 'TextInput_Response_1234' const referenceLuIntent: FieldProcessorAsync = async (fromId, fromAction, toId, toAction) => { @@ -106,7 +102,7 @@ export const useActionApi = (projectId: string) => { async function copyActions(dialogId: string, actions: MicrosoftIDialog[]) { // 'SendActivity_1234' -> '- hi' const dereferenceLg: FieldProcessorAsync = async (fromId, fromAction, toId, toAction, lgFieldName) => - readLgTemplate(fromAction[lgFieldName] as string); + readLgTemplate(fromAction[lgFieldName] as string, fromId); // 'TextInput_Response_1234' -> LuIntentSection | undefined const dereferenceLu: FieldProcessorAsync = async (fromId, fromAction, toId, toAction) => { @@ -129,10 +125,6 @@ export const useActionApi = (projectId: string) => { return copiedAction; } - async function deleteAction(dialogId: string, action: MicrosoftIDialog) { - return deleteActions(dialogId, [action]); - } - async function deleteActions(dialogId: string, actions: MicrosoftIDialog[]) { actions.forEach(({ $kind }) => { TelemetryClient.track('ActionDeleted', { type: $kind }); @@ -144,6 +136,10 @@ export const useActionApi = (projectId: string) => { ); } + async function deleteAction(dialogId: string, action: MicrosoftIDialog) { + return deleteActions(dialogId, [action]); + } + return { constructAction, constructActions, diff --git a/Composer/packages/client/src/shell/utils.ts b/Composer/packages/client/src/shell/utils.ts new file mode 100644 index 0000000000..ec170d4c7f --- /dev/null +++ b/Composer/packages/client/src/shell/utils.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LgMetaData, LgTemplateRef, LgType } from '@bfc/shared'; +import { LgTemplate, MicrosoftIDialog, ShellApi } from '@botframework-composer/types'; + +type SerializableLg = { + originalId: string; + mainTemplateBody?: string; + relatedLgTemplateBodies?: Record; +}; + +/** + * Serializes Lg template to JSON format. + * @param templateName Name of the template to be serialized. + * @param fromId Original id of the wrapper template. + * @param lgText Body of the template. + * @param lgTemplates List of all available Lg templates + * @returns A serialized string representing the Lg template. + */ +export const serializeLgTemplate = ( + templateName: string, + fromId: string, + lgText: string, + lgTemplates: LgTemplate[] +) => { + const lgTemplate = lgTemplates.find((x) => x.name === templateName); + + if (!lgTemplate) { + return ''; + } + + const exprRegex = /^\${(.*)\(\)}$/; + const serializableLg: SerializableLg = { + originalId: fromId, + mainTemplateBody: lgTemplate?.body, + }; + + // This section serializes structured responses. + if (lgTemplate?.properties?.$type === 'Activity') { + for (const responseType of ['Text', 'Speak', 'Attachments']) { + if (lgTemplate.properties[responseType]) { + const subTemplateItems = Array.isArray(lgTemplate.properties[responseType]) + ? (lgTemplate.properties[responseType] as string[]) + : ([lgTemplate.properties[responseType]] as string[]); + for (const subTemplateItem of subTemplateItems) { + const matched = subTemplateItem.trim().match(exprRegex); + if (matched && matched.length > 1) { + const subTemplateId = matched[1]; + const subTemplate = lgTemplates.find((x) => x.name === subTemplateId); + if (subTemplate) { + if (!serializableLg.relatedLgTemplateBodies) { + serializableLg.relatedLgTemplateBodies = {}; + } + serializableLg.relatedLgTemplateBodies[subTemplateId] = subTemplate.body; + } + } + } + } + } + } + + return lgTemplate ? JSON.stringify(serializableLg) : lgText; +}; + +/** + * Deserialize serialized Lg template and create all required templates. + * @param lgFileId Lg file id that hosts the template. + * @param toId New wrapper if for the Lg template. + * @param lgText Serialized body of the template. + * @param hostActionData Hosting dialog data. + * @param hostFieldName Hosting field name. + * @param addLgTemplate Api for creating a new template. + * @returns Deserialized template expression. + */ +export const deserializeLgTemplate = async ( + lgFileId: string, + toId: string, + lgText: string, + hostActionData: MicrosoftIDialog, + hostFieldName: string, + addLgTemplate: ShellApi['addLgTemplate'] +) => { + const newLgType = new LgType(hostActionData.$kind, hostFieldName).toString(); + const newLgTemplateName = new LgMetaData(newLgType, toId).toString(); + const newLgTemplateRefStr = new LgTemplateRef(newLgTemplateName).toString(); + + try { + const serializableLg = JSON.parse(lgText) as SerializableLg; + // It's a serialized JSON string + const { originalId, mainTemplateBody, relatedLgTemplateBodies } = serializableLg; + + const pattern = `${originalId}`; + // eslint-disable-next-line security/detect-non-literal-regexp + const regex = new RegExp(pattern, 'g'); + + // Re-create related Lg templates + if (relatedLgTemplateBodies) { + for (const subTemplateId of Object.keys(relatedLgTemplateBodies)) { + const subTemplateBody = relatedLgTemplateBodies[subTemplateId]; + await addLgTemplate(lgFileId, subTemplateId.replace(regex, toId), subTemplateBody); + } + } + + // Create the target Lg template + await addLgTemplate(lgFileId, newLgTemplateName, mainTemplateBody?.replace(regex, toId) ?? ''); + } catch { + // It's a normal string, just create the target Lg template + await addLgTemplate(lgFileId, newLgTemplateName, lgText); + } + return newLgTemplateRefStr; +};