Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions Composer/packages/client/src/shell/actionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import {
deepCopyActions,
deleteActions as destructActions,
FieldProcessorAsync,
walkAdaptiveActionList,
LgType,
LgMetaData,
LgTemplateRef,
LuType,
LuMetaData,
LuType,
walkAdaptiveActionList,
} from '@bfc/shared';
import { LuIntentSection, MicrosoftIDialog } from '@botframework-composer/types';

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);
Expand All @@ -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<string> => {
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);
Expand All @@ -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
Expand All @@ -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;
};

Expand All @@ -90,7 +86,7 @@ export const useActionApi = (projectId: string) => {
});
// '- hi' -> 'SendActivity_1234'
const referenceLgText: FieldProcessorAsync<string> = 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<any> = async (fromId, fromAction, toId, toAction) => {
Expand All @@ -106,7 +102,7 @@ export const useActionApi = (projectId: string) => {
async function copyActions(dialogId: string, actions: MicrosoftIDialog[]) {
// 'SendActivity_1234' -> '- hi'
const dereferenceLg: FieldProcessorAsync<string> = 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<any> = async (fromId, fromAction, toId, toAction) => {
Expand All @@ -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 });
Expand All @@ -144,6 +136,10 @@ export const useActionApi = (projectId: string) => {
);
}

async function deleteAction(dialogId: string, action: MicrosoftIDialog) {
return deleteActions(dialogId, [action]);
}

return {
constructAction,
constructActions,
Expand Down
112 changes: 112 additions & 0 deletions Composer/packages/client/src/shell/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
};

/**
* 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;
};