diff --git a/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/IDialogArray.test.tsx b/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/IDialogArray.test.tsx
index a72cdccd03..a3e294a692 100644
--- a/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/IDialogArray.test.tsx
+++ b/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/IDialogArray.test.tsx
@@ -74,12 +74,14 @@ describe('', () => {
id: expect.any(String),
name: 'Send a response',
},
+ activity: '',
data: {
$type: 'Microsoft.SendActivity',
$designer: {
id: expect.any(String),
name: 'Send a response',
},
+ activity: '',
},
key: 'Microsoft.SendActivity',
name: 'Send a response',
diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
index 02cf4b943b..87ee086f21 100644
--- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
+++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
@@ -45,10 +45,9 @@ export const ObiEditor: FC = ({
}): JSX.Element | null => {
let divRef;
- const { focusedId, focusedEvent, clipboardActions, updateLgTemplate, getLgTemplates, removeLgTemplates } = useContext(
+ const { focusedId, focusedEvent, clipboardActions, copyLgTemplate, removeLgTemplates } = useContext(
NodeRendererContext
);
- const lgApi = { getLgTemplates, removeLgTemplates, updateLgTemplate };
const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => {
let handler;
switch (eventName) {
@@ -107,7 +106,19 @@ export const ObiEditor: FC = ({
case NodeEventTypes.Insert:
if (eventData.$type === 'PASTE') {
handler = e => {
- pasteNodes(data, e.id, e.position, clipboardActions, lgApi).then(dialog => {
+ // TODO: clean this along with node deletion.
+ const copyLgTemplateToNewNode = async (lgTemplateName: string, newNodeId: string) => {
+ const matches = /\[(bfd\w+-(\d+))\]/.exec(lgTemplateName);
+ if (Array.isArray(matches) && matches.length === 3) {
+ const originLgId = matches[1];
+ const originNodeId = matches[2];
+ const newLgId = originLgId.replace(originNodeId, newNodeId);
+ await copyLgTemplate('common', originLgId, newLgId);
+ return `[${newLgId}]`;
+ }
+ return lgTemplateName;
+ };
+ pasteNodes(data, e.id, e.position, clipboardActions, copyLgTemplateToNewNode).then(dialog => {
onChange(dialog);
});
};
diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx
index 5147b34eaf..0d46bc58a7 100644
--- a/Composer/packages/extensions/visual-designer/src/index.tsx
+++ b/Composer/packages/extensions/visual-designer/src/index.tsx
@@ -53,6 +53,7 @@ const VisualDesigner: React.FC = ({
saveData,
updateLgTemplate,
getLgTemplates,
+ copyLgTemplate,
removeLgTemplate,
removeLgTemplates,
undo,
@@ -69,6 +70,7 @@ const VisualDesigner: React.FC = ({
clipboardActions: clipboardActions || [],
updateLgTemplate,
getLgTemplates,
+ copyLgTemplate,
removeLgTemplate,
removeLgTemplates,
});
diff --git a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
index dc2b30c286..535ed58533 100644
--- a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
+++ b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
@@ -14,6 +14,7 @@ export const NodeRendererContext = React.createContext({
focusedTab: '',
clipboardActions: [] as any[],
getLgTemplates: (_id: string, _templateName: string) => Promise.resolve([] as LgTemplate[]),
+ copyLgTemplate: (_id: string, _fromTemplateName: string, _toTemplateName: string) => Promise.resolve(''),
removeLgTemplate: (_id: string, _templateName: string) => Promise.resolve(),
removeLgTemplates: (_id: string, _templateNames: string[]) => Promise.resolve(),
updateLgTemplate: (_id: string, _templateName: string, _template: string) => Promise.resolve('' as string),
diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts
index bffe04aeea..b38aad4044 100644
--- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts
+++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts
@@ -201,7 +201,7 @@ export function appendNodesAfter(inputDialog, targetId, newNodes) {
return dialog;
}
-export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, lgApi) {
+export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, copyLgTemplate) {
if (!Array.isArray(newNodes) || newNodes.length === 0) {
return inputDialog;
}
@@ -219,7 +219,7 @@ export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, l
const copiedNodes: any[] = [];
for (const node of newNodes) {
// Deep copy nodes with external resources
- const copy = await deepCopyAction(node, lgApi);
+ const copy = await deepCopyAction(node, copyLgTemplate);
copiedNodes.push(copy);
}
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils.test.ts
deleted file mode 100644
index 33c3301931..0000000000
--- a/Composer/packages/lib/shared/__tests__/copyUtils.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-import { copyAdaptiveAction } from '../src/copyUtils';
-
-describe('copyAdaptiveAction', () => {
- const lgTemplate = [{ Name: 'bfdactivity-1234', Body: '-hello' }, { Name: 'bfdprompt-1234', Body: '-hi' }];
- const externalApi = {
- updateDesigner: data => {
- data.$designer = { id: '5678' };
- },
- lgApi: {
- getLgTemplates: (fileId, activityId) => {
- return Promise.resolve(lgTemplate);
- },
- updateLgTemplate: (filedId, activityId, activityBody) => {
- return Promise.resolve(true);
- },
- },
- };
- const externalApiWithFailure = {
- ...externalApi,
- lgApi: {
- ...externalApi.lgApi,
- updateLgTemplate: () => Promise.reject(),
- },
- };
-
- it('should return {} when input is invalid', async () => {
- expect(await copyAdaptiveAction(null, externalApi)).toEqual({});
- expect(await copyAdaptiveAction({}, externalApi)).toEqual({});
- expect(await copyAdaptiveAction({ name: 'hi' }, externalApi)).toEqual({});
- });
-
- it('can copy BeginDialog', async () => {
- const beginDialog = {
- $type: 'Microsoft.BeginDialog',
- dialog: 'AddToDo',
- };
-
- expect(await copyAdaptiveAction(beginDialog, externalApi)).toEqual({
- $type: 'Microsoft.BeginDialog',
- $designer: { id: '5678' },
- dialog: 'AddToDo',
- });
- });
-
- it('can copy SendActivity', async () => {
- const sendActivity = {
- $type: 'Microsoft.SendActivity',
- activity: '[bfdactivity-1234]',
- };
-
- expect(await copyAdaptiveAction(sendActivity, externalApi)).toEqual({
- $type: 'Microsoft.SendActivity',
- $designer: { id: '5678' },
- activity: '[bfdactivity-5678]',
- });
-
- expect(await copyAdaptiveAction(sendActivity, externalApiWithFailure)).toEqual({
- $type: 'Microsoft.SendActivity',
- $designer: { id: '5678' },
- activity: '-hello',
- });
- });
-
- it('can copy TextInput', async () => {
- const promptText = {
- $type: 'Microsoft.TextInput',
- $designer: {
- id: '844184',
- name: 'Prompt for text',
- },
- maxTurnCount: 3,
- alwaysPrompt: false,
- allowInterruptions: 'true',
- outputFormat: 'none',
- prompt: '[bfdprompt-1234]',
- };
-
- expect(await copyAdaptiveAction(promptText, externalApi)).toEqual({
- $type: 'Microsoft.TextInput',
- $designer: {
- id: '5678',
- },
- maxTurnCount: 3,
- alwaysPrompt: false,
- allowInterruptions: 'true',
- outputFormat: 'none',
- prompt: '[bfdprompt-5678]',
- });
-
- expect(await copyAdaptiveAction(promptText, externalApiWithFailure)).toEqual({
- $type: 'Microsoft.TextInput',
- $designer: {
- id: '5678',
- },
- maxTurnCount: 3,
- alwaysPrompt: false,
- allowInterruptions: 'true',
- outputFormat: 'none',
- prompt: '-hi',
- });
- });
-});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyAdaptiveAction.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyAdaptiveAction.test.ts
new file mode 100644
index 0000000000..8e459f54b7
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyAdaptiveAction.test.ts
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+import { SDKTypes } from '../../src';
+import CopyConstructorMap from '../../src/copyUtils/CopyConstructorMap';
+import { copyAdaptiveAction } from '../../src/copyUtils';
+
+// NOTES: Cannot use SDKTypes here. `jest.mock` has to have zero dependency.
+jest.mock('../../src/copyUtils/CopyConstructorMap', () => ({
+ 'Microsoft.SendActivity': jest.fn(),
+ 'Microsoft.IfCondition': jest.fn(),
+ 'Microsoft.SwitchCondition': jest.fn(),
+ 'Microsoft.EditActions': jest.fn(),
+ 'Microsoft.ChoiceInput': jest.fn(),
+ 'Microsoft.Foreach': jest.fn(),
+ default: jest.fn(),
+}));
+
+describe('copyAdaptiveAction', () => {
+ it('should return {} when input is invalid', async () => {
+ expect(await copyAdaptiveAction('hello', externalApi)).toEqual('hello');
+
+ expect(await copyAdaptiveAction(null as any, externalApi)).toEqual({});
+ expect(await copyAdaptiveAction({} as any, externalApi)).toEqual({});
+ expect(await copyAdaptiveAction({ name: 'hi' } as any, externalApi)).toEqual({});
+ });
+
+ const registeredTypes = [
+ SDKTypes.SendActivity,
+ SDKTypes.IfCondition,
+ SDKTypes.SwitchCondition,
+ SDKTypes.EditActions,
+ SDKTypes.ChoiceInput,
+ SDKTypes.Foreach,
+ ];
+ for (const $type of registeredTypes) {
+ it(`should invoke registered handler for ${$type}`, async () => {
+ await copyAdaptiveAction({ $type }, externalApi);
+ expect(CopyConstructorMap[$type]).toHaveReturnedTimes(1);
+ });
+ }
+
+ it('should invoke default handler for other types', async () => {
+ await copyAdaptiveAction({ $type: SDKTypes.BeginDialog }, externalApi);
+ expect(CopyConstructorMap.default).toHaveReturnedTimes(1);
+
+ await copyAdaptiveAction({ $type: SDKTypes.HttpRequest }, externalApi);
+ expect(CopyConstructorMap.default).toHaveReturnedTimes(2);
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyEditActions.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyEditActions.test.ts
new file mode 100644
index 0000000000..94280afe63
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyEditActions.test.ts
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { copyEditActions } from '../../src/copyUtils/copyEditActions';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('#copyEditActions', () => {
+ it('can copy EditActions', async () => {
+ const editActions = {
+ $type: 'Microsoft.EditActions',
+ changeType: 'InsertActions',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ dialog: 'AddToDo',
+ },
+ ],
+ };
+
+ expect(await copyEditActions(editActions, externalApi)).toEqual({
+ $type: 'Microsoft.EditActions',
+ $designer: {
+ id: '5678',
+ },
+ changeType: 'InsertActions',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ $designer: {
+ id: '5678',
+ },
+ dialog: 'AddToDo',
+ },
+ ],
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyForeach.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyForeach.test.ts
new file mode 100644
index 0000000000..bdf2a8fca6
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyForeach.test.ts
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License
+
+import { copyForeach } from '../../src/copyUtils/copyForeach';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('#copyForeach', () => {
+ it('can copy Foreach action', async () => {
+ const foreachInstance = {
+ $type: 'Microsoft.Foreach',
+ itemsProperty: 'name',
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ activity: 'hello',
+ },
+ ],
+ };
+
+ expect(await copyForeach(foreachInstance, externalApi)).toEqual({
+ $type: 'Microsoft.Foreach',
+ itemsProperty: 'name',
+ $designer: {
+ id: '5678',
+ },
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ $designer: {
+ id: '5678',
+ },
+ activity: 'hello',
+ },
+ ],
+ });
+ });
+
+ it('can copy ForeachPage action', async () => {
+ const foreachPageInstance = {
+ $type: 'Microsoft.Foreach',
+ itemsProperty: 'name',
+ pageSize: 10,
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ activity: 'hello',
+ },
+ ],
+ };
+
+ expect(await copyForeach(foreachPageInstance, externalApi)).toEqual({
+ $type: 'Microsoft.Foreach',
+ itemsProperty: 'name',
+ pageSize: 10,
+ $designer: {
+ id: '5678',
+ },
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ $designer: {
+ id: '5678',
+ },
+ activity: 'hello',
+ },
+ ],
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyIfCondition.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyIfCondition.test.ts
new file mode 100644
index 0000000000..369246d0c8
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyIfCondition.test.ts
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { copyIfCondition } from '../../src/copyUtils/copyIfCondition';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('#copyIfCondition', () => {
+ it('can copy normal input', async () => {
+ const ifCondition = {
+ $type: 'Microsoft.IfCondition',
+ condition: 'a == b',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ dialog: 'AddToDo',
+ },
+ ],
+ elseActions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ activity: '[bfdactivity-1234]',
+ },
+ ],
+ };
+
+ expect(await copyIfCondition(ifCondition, externalApi)).toEqual({
+ $type: 'Microsoft.IfCondition',
+ $designer: {
+ id: '5678',
+ },
+ condition: 'a == b',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ $designer: {
+ id: '5678',
+ },
+ dialog: 'AddToDo',
+ },
+ ],
+ elseActions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ $designer: {
+ id: '5678',
+ },
+ activity: '[bfdactivity-1234]',
+ },
+ ],
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts
new file mode 100644
index 0000000000..918ef04eaf
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License
+
+import { copyInputDialog } from '../../src/copyUtils/copyInputDialog';
+import { ExternalApi } from '../../src/copyUtils/ExternalApi';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('shallowCopyAdaptiveAction', () => {
+ const externalApiWithLgCopy: ExternalApi = {
+ ...externalApi,
+ copyLgTemplate: (templateName, newNodeId) => Promise.resolve(templateName + '(copy)'),
+ };
+
+ it('can copy TextInput', async () => {
+ const promptText = {
+ $type: 'Microsoft.TextInput',
+ $designer: {
+ id: '844184',
+ name: 'Prompt for text',
+ },
+ maxTurnCount: 3,
+ alwaysPrompt: false,
+ allowInterruptions: 'true',
+ outputFormat: 'none',
+ prompt: '[bfdprompt-1234]',
+ invalidPrompt: '[bfdinvalidPrompt-1234]',
+ unrecognizedPrompt: '[bfdunrecognizedPrompt-1234]',
+ defaultValueResponse: '[bfddefaultValueResponse-1234]',
+ };
+
+ expect(await copyInputDialog(promptText as any, externalApiWithLgCopy)).toEqual({
+ $type: 'Microsoft.TextInput',
+ $designer: {
+ id: '5678',
+ },
+ maxTurnCount: 3,
+ alwaysPrompt: false,
+ allowInterruptions: 'true',
+ outputFormat: 'none',
+ prompt: '[bfdprompt-1234](copy)',
+ invalidPrompt: '[bfdinvalidPrompt-1234](copy)',
+ unrecognizedPrompt: '[bfdunrecognizedPrompt-1234](copy)',
+ defaultValueResponse: '[bfddefaultValueResponse-1234](copy)',
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts
new file mode 100644
index 0000000000..8a30e66c53
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License
+
+import { copySendActivity } from '../../src/copyUtils/copySendActivity';
+import { ExternalApi } from '../../src/copyUtils/ExternalApi';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('copySendActivity', () => {
+ const externalApiWithLgCopy: ExternalApi = {
+ ...externalApi,
+ copyLgTemplate: (templateName, newNodeId) => Promise.resolve(templateName + '(copy)'),
+ };
+
+ it('can copy SendActivity', async () => {
+ const sendActivity = {
+ $type: 'Microsoft.SendActivity',
+ activity: '[bfdactivity-1234]',
+ };
+
+ expect(await copySendActivity(sendActivity, externalApiWithLgCopy)).toEqual({
+ $type: 'Microsoft.SendActivity',
+ $designer: { id: '5678' },
+ activity: '[bfdactivity-1234](copy)',
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copySwitchCondition.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copySwitchCondition.test.ts
new file mode 100644
index 0000000000..3a2900b619
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/copySwitchCondition.test.ts
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { copySwitchCondition } from '../../src/copyUtils/copySwitchCondition';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('#copySwitchCondition', () => {
+ it('can copy cases and default in input', async () => {
+ const switchCondition = {
+ $type: 'Microsoft.SwitchCondition',
+ condition: 'dialog.x',
+ default: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ dialog: 'AddToDo',
+ },
+ {
+ $type: 'Microsoft.IfCondition',
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ activity: '[bfdactivity-1234]',
+ },
+ ],
+ },
+ ],
+ cases: [
+ {
+ value: '0',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ dialog: 'AddToDo',
+ },
+ ],
+ },
+ {
+ value: '1',
+ actions: [
+ {
+ $type: 'Microsoft.SwitchCondition',
+ condition: 'a.b',
+ default: [],
+ cases: [],
+ },
+ ],
+ },
+ ],
+ };
+
+ expect(await copySwitchCondition(switchCondition, externalApi)).toEqual({
+ $type: 'Microsoft.SwitchCondition',
+ $designer: {
+ id: '5678',
+ },
+ condition: 'dialog.x',
+ default: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ $designer: {
+ id: '5678',
+ },
+ dialog: 'AddToDo',
+ },
+ {
+ $type: 'Microsoft.IfCondition',
+ $designer: {
+ id: '5678',
+ },
+ actions: [
+ {
+ $type: 'Microsoft.SendActivity',
+ $designer: {
+ id: '5678',
+ },
+ activity: '[bfdactivity-1234]',
+ },
+ ],
+ },
+ ],
+ cases: [
+ {
+ value: '0',
+ actions: [
+ {
+ $type: 'Microsoft.BeginDialog',
+ $designer: {
+ id: '5678',
+ },
+ dialog: 'AddToDo',
+ },
+ ],
+ },
+ {
+ value: '1',
+ actions: [
+ {
+ $type: 'Microsoft.SwitchCondition',
+ $designer: {
+ id: '5678',
+ },
+ condition: 'a.b',
+ default: [],
+ cases: [],
+ },
+ ],
+ },
+ ],
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/shallowCopyAdaptiveAction.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/shallowCopyAdaptiveAction.test.ts
new file mode 100644
index 0000000000..5ded14c015
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/copyUtils/shallowCopyAdaptiveAction.test.ts
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License
+
+import { shallowCopyAdaptiveAction } from '../../src/copyUtils/shallowCopyAdaptiveAction';
+import { externalApiStub as externalApi } from '../jestMocks/externalApiStub';
+
+describe('shallowCopyAdaptiveAction', () => {
+ it('can copy BeginDialog', () => {
+ const beginDialog = {
+ $type: 'Microsoft.BeginDialog',
+ dialog: 'AddToDo',
+ };
+
+ expect(shallowCopyAdaptiveAction(beginDialog, externalApi)).toEqual({
+ $type: 'Microsoft.BeginDialog',
+ $designer: { id: '5678' },
+ dialog: 'AddToDo',
+ });
+ });
+});
diff --git a/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts b/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts
new file mode 100644
index 0000000000..c8e297b99d
--- /dev/null
+++ b/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License
+
+import { ExternalApi } from '../../src/copyUtils/ExternalApi';
+
+export const externalApiStub: ExternalApi = {
+ getDesignerId: () => ({ id: '5678' }),
+ copyLgTemplate: (lgTemplateName: string, targetNodeId: string) => Promise.resolve(lgTemplateName),
+};
diff --git a/Composer/packages/lib/shared/jest.config.js b/Composer/packages/lib/shared/jest.config.js
index 01191b6597..c1f7a38283 100644
--- a/Composer/packages/lib/shared/jest.config.js
+++ b/Composer/packages/lib/shared/jest.config.js
@@ -2,7 +2,7 @@ const path = require('path');
module.exports = {
preset: 'ts-jest/presets/js-with-babel',
- testPathIgnorePatterns: ['/node_modules/'],
+ testPathIgnorePatterns: ['/node_modules/', '/jestMocks/'],
watchPathIgnorePatterns: ['/__tests__/mocks'],
moduleNameMapper: {
// Any imports of .scss / .css files will instead import styleMock.js which is an empty object
diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts
deleted file mode 100644
index 99f4aff179..0000000000
--- a/Composer/packages/lib/shared/src/copyUtils.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-const NestedFieldNames = {
- Actions: 'actions',
- ElseActions: 'elseActions',
- DefaultCase: 'default',
- Cases: 'cases',
-};
-
-const DEFAULT_CHILDREN_KEYS = [NestedFieldNames.Actions];
-const childrenMap = {
- ['Microsoft.IfCondition']: [NestedFieldNames.Actions, NestedFieldNames.ElseActions],
- ['Microsoft.SwitchCondition']: [NestedFieldNames.Cases, NestedFieldNames.DefaultCase],
-};
-
-/**
- * Considering that an Adaptive Action could be nested with other actions,
- * for example, the IfCondition and SwitchCondition and Foreach, we need
- * this helper to visit all possible action nodes recursively.
- *
- * @param {any} input The input Adaptive Action which has $type field.
- * @param {function} visitor The callback function called on each action node.
- */
-async function walkAdaptiveAction(input: any, visitor: (data: any) => Promise) {
- if (!input || !input.$type) return;
-
- await visitor(input);
-
- let childrenKeys = DEFAULT_CHILDREN_KEYS;
- if (input.$type && childrenMap[input.$type]) {
- childrenKeys = childrenMap[input.$type];
- }
-
- for (const childrenKey of childrenKeys) {
- const children = input[childrenKey];
- if (Array.isArray(children)) {
- Promise.all(children.map(async x => await walkAdaptiveAction(x, visitor)));
- }
- }
-}
-
-const TEMPLATE_PATTERN = /^\[bfd(.+)-(\d+)\]$/;
-function isLgTemplate(template: string): boolean {
- return TEMPLATE_PATTERN.test(template);
-}
-
-function parseLgTemplate(template: string) {
- const result = TEMPLATE_PATTERN.exec(template);
- if (result && result.length === 3) {
- return {
- templateType: result[1],
- templateId: result[2],
- };
- }
- return null;
-}
-
-async function copyLgActivity(activity: string, designerId: string, lgApi: any): Promise {
- if (!activity) return '';
- if (!lgApi) return activity;
-
- const lgTemplate = parseLgTemplate(activity);
- if (!lgTemplate) return activity;
-
- const { templateType } = lgTemplate;
- const { getLgTemplates, updateLgTemplate } = lgApi;
- if (!getLgTemplates) return activity;
-
- let rawLg: any[] = [];
- try {
- rawLg = await getLgTemplates('common', activity);
- } catch (error) {
- return activity;
- }
-
- const currentLg = rawLg.find(lg => `[${lg.Name}]` === activity);
-
- if (currentLg) {
- // Create new lg activity.
- const newLgContent = currentLg.Body;
- const newLgId = `bfd${templateType}-${designerId}`;
- try {
- await updateLgTemplate('common', newLgId, newLgContent);
- return `[${newLgId}]`;
- } catch (e) {
- return newLgContent;
- }
- }
- return activity;
-}
-
-const overrideLgActivity = async (data, { lgApi }) => {
- data.activity = await copyLgActivity(data.activity, data.$designer.id, lgApi);
-};
-
-const overrideLgPrompt = async (data, { lgApi }) => {
- const promptFields = ['prompt', 'unrecognizedPrompt', 'defaultValueResponse', 'invalidPrompt'];
- for (const field of promptFields) {
- if (isLgTemplate(data[field])) {
- data[field] = await copyLgActivity(data[field], data.$designer.id, lgApi);
- }
- }
-};
-
-// TODO: use $type from SDKTypes (after solving circular import issue).
-const OverriderByType = {
- 'Microsoft.SendActivity': overrideLgActivity,
- 'Microsoft.AttachmentInput': overrideLgPrompt,
- 'Microsoft.ConfirmInput': overrideLgPrompt,
- 'Microsoft.DateTimeInput': overrideLgPrompt,
- 'Microsoft.NumberInput': overrideLgPrompt,
- 'Microsoft.OAuthInput': overrideLgPrompt,
- 'Microsoft.TextInput': overrideLgPrompt,
- 'Microsoft.ChoiceInput': overrideLgPrompt,
-};
-
-const needsOverride = data => !!(data && OverriderByType[data.$type]);
-
-export async function copyAdaptiveAction(data, externalApi) {
- if (!data || !data.$type) return {};
-
- // Deep copy the original data.
- const copy = JSON.parse(JSON.stringify(data));
-
- const { updateDesigner } = externalApi;
- // Create copy handler for rewriting fields which need to be handled specially.
- const copyHandler = async data => {
- updateDesigner(data);
- if (needsOverride(data)) {
- const overrider = OverriderByType[data.$type];
- await overrider(data, externalApi);
- }
- };
-
- // Walk action and rewrite needs copy fields
- await walkAdaptiveAction(copy, copyHandler);
-
- return copy;
-}
diff --git a/Composer/packages/lib/shared/src/copyUtils/CopyConstructorMap.ts b/Composer/packages/lib/shared/src/copyUtils/CopyConstructorMap.ts
new file mode 100644
index 0000000000..934aef2f8c
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/CopyConstructorMap.ts
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { SDKTypes } from '../types/schema';
+
+import { copySendActivity } from './copySendActivity';
+import { copyInputDialog } from './copyInputDialog';
+import { copyIfCondition } from './copyIfCondition';
+import { copySwitchCondition } from './copySwitchCondition';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+import { copyForeach } from './copyForeach';
+import { copyEditActions } from './copyEditActions';
+
+const CopyConstructorMap = {
+ [SDKTypes.SendActivity]: copySendActivity,
+ [SDKTypes.AttachmentInput]: copyInputDialog,
+ [SDKTypes.ChoiceInput]: copyInputDialog,
+ [SDKTypes.ConfirmInput]: copyInputDialog,
+ [SDKTypes.DateTimeInput]: copyInputDialog,
+ [SDKTypes.NumberInput]: copyInputDialog,
+ [SDKTypes.TextInput]: copyInputDialog,
+ [SDKTypes.IfCondition]: copyIfCondition,
+ [SDKTypes.SwitchCondition]: copySwitchCondition,
+ [SDKTypes.Foreach]: copyForeach,
+ [SDKTypes.ForeachPage]: copyForeach,
+ [SDKTypes.EditActions]: copyEditActions,
+ default: shallowCopyAdaptiveAction,
+};
+
+export default CopyConstructorMap;
diff --git a/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts b/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts
new file mode 100644
index 0000000000..2f86b9aa01
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { DesignerData } from '../types';
+
+export interface ExternalApi {
+ getDesignerId: (data?: DesignerData) => DesignerData;
+ copyLgTemplate: (lgTemplateName: string, newNodeId: string) => Promise;
+}
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveAction.ts b/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveAction.ts
new file mode 100644
index 0000000000..b2cd61a68d
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveAction.ts
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MicrosoftIDialog } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import CopyConstructorMap from './CopyConstructorMap';
+
+export async function copyAdaptiveAction(data: MicrosoftIDialog, externalApi: ExternalApi): Promise {
+ if (typeof data === 'string') {
+ return data;
+ }
+
+ if (!data || !data.$type) return {};
+
+ const copier = CopyConstructorMap[data.$type] || CopyConstructorMap.default;
+
+ return await copier(data, externalApi);
+}
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveActionList.ts b/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveActionList.ts
new file mode 100644
index 0000000000..c6c5290df6
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyAdaptiveActionList.ts
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MicrosoftIDialog } from '../types';
+
+import { copyAdaptiveAction } from './copyAdaptiveAction';
+import { ExternalApi } from './ExternalApi';
+
+export async function copyAdaptiveActionList(
+ actions: MicrosoftIDialog[],
+ externalApi: ExternalApi
+): Promise {
+ if (!Array.isArray(actions)) return [];
+
+ const results: MicrosoftIDialog[] = [];
+ for (const action of actions) {
+ const copy = await copyAdaptiveAction(action, externalApi);
+ results.push(copy);
+ }
+ return results;
+}
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyEditActions.ts b/Composer/packages/lib/shared/src/copyUtils/copyEditActions.ts
new file mode 100644
index 0000000000..d35bb76d5c
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyEditActions.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { EditActions } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+import { copyAdaptiveActionList } from './copyAdaptiveActionList';
+
+export const copyEditActions = async (input: EditActions, externalApi: ExternalApi): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+
+ if (Array.isArray(input.actions)) {
+ copy.actions = await copyAdaptiveActionList(input.actions, externalApi);
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyForeach.ts b/Composer/packages/lib/shared/src/copyUtils/copyForeach.ts
new file mode 100644
index 0000000000..1c4b71c499
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyForeach.ts
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Foreach, ForeachPage } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+import { copyAdaptiveActionList } from './copyAdaptiveActionList';
+
+type ForeachAction = Foreach | ForeachPage;
+export const copyForeach = async (input: ForeachAction, externalApi: ExternalApi): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+
+ if (Array.isArray(input.actions)) {
+ copy.actions = await copyAdaptiveActionList(input.actions, externalApi);
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyIfCondition.ts b/Composer/packages/lib/shared/src/copyUtils/copyIfCondition.ts
new file mode 100644
index 0000000000..b5aa2bce9a
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyIfCondition.ts
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { IfCondition } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { copyAdaptiveActionList } from './copyAdaptiveActionList';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+
+export const copyIfCondition = async (input: IfCondition, externalApi: ExternalApi): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+
+ if (Array.isArray(input.actions)) {
+ copy.actions = await copyAdaptiveActionList(input.actions, externalApi);
+ }
+
+ if (Array.isArray(input.elseActions)) {
+ copy.elseActions = await copyAdaptiveActionList(input.elseActions, externalApi);
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts
new file mode 100644
index 0000000000..5371b69e04
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { InputDialog } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+
+export const copyInputDialog = async (input: InputDialog, externalApi: ExternalApi): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+ const nodeId = copy.$designer ? copy.$designer.id : '';
+
+ if (input.prompt !== undefined) {
+ copy.prompt = await externalApi.copyLgTemplate(input.prompt, nodeId);
+ }
+
+ if (input.unrecognizedPrompt !== undefined) {
+ copy.unrecognizedPrompt = await externalApi.copyLgTemplate(input.unrecognizedPrompt, nodeId);
+ }
+
+ if (input.invalidPrompt !== undefined) {
+ copy.invalidPrompt = await externalApi.copyLgTemplate(input.invalidPrompt, nodeId);
+ }
+
+ if (input.defaultValueResponse !== undefined) {
+ copy.defaultValueResponse = await externalApi.copyLgTemplate(input.defaultValueResponse, nodeId);
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts b/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts
new file mode 100644
index 0000000000..54c22113c4
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { SendActivity } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+
+export const copySendActivity = async (input: SendActivity, externalApi: ExternalApi): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+ const nodeId = copy.$designer ? copy.$designer.id : '';
+
+ if (input.activity !== undefined) {
+ copy.activity = await externalApi.copyLgTemplate(input.activity, nodeId);
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/copySwitchCondition.ts b/Composer/packages/lib/shared/src/copyUtils/copySwitchCondition.ts
new file mode 100644
index 0000000000..02c4af4e76
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/copySwitchCondition.ts
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { SwitchCondition, CaseCondition } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+import { copyAdaptiveActionList } from './copyAdaptiveActionList';
+import { shallowCopyAdaptiveAction } from './shallowCopyAdaptiveAction';
+
+export const copySwitchCondition = async (
+ input: SwitchCondition,
+ externalApi: ExternalApi
+): Promise => {
+ const copy = shallowCopyAdaptiveAction(input, externalApi);
+
+ if (Array.isArray(input.default)) {
+ copy.default = await copyAdaptiveActionList(input.default, externalApi);
+ }
+
+ if (Array.isArray(input.cases)) {
+ const copiedCases: CaseCondition[] = [];
+ for (const caseCondition of input.cases) {
+ copiedCases.push({
+ value: caseCondition.value,
+ actions: await copyAdaptiveActionList(caseCondition.actions, externalApi),
+ });
+ }
+ copy.cases = copiedCases;
+ }
+
+ return copy;
+};
diff --git a/Composer/packages/lib/shared/src/copyUtils/index.ts b/Composer/packages/lib/shared/src/copyUtils/index.ts
new file mode 100644
index 0000000000..c04dd59fa5
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/index.ts
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export { copyAdaptiveAction } from './copyAdaptiveAction';
diff --git a/Composer/packages/lib/shared/src/copyUtils/shallowCopyAdaptiveAction.ts b/Composer/packages/lib/shared/src/copyUtils/shallowCopyAdaptiveAction.ts
new file mode 100644
index 0000000000..b5c198a39c
--- /dev/null
+++ b/Composer/packages/lib/shared/src/copyUtils/shallowCopyAdaptiveAction.ts
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { BaseSchema } from '../types';
+
+import { ExternalApi } from './ExternalApi';
+
+export function shallowCopyAdaptiveAction(input: T, externalApi: ExternalApi): T {
+ return {
+ ...input,
+ $designer: externalApi.getDesignerId(input.$designer),
+ };
+}
diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts
index 6b6ec6aeda..d7b814612f 100644
--- a/Composer/packages/lib/shared/src/dialogFactory.ts
+++ b/Composer/packages/lib/shared/src/dialogFactory.ts
@@ -3,6 +3,7 @@
import nanoid from 'nanoid/generate';
+import { DesignerData } from './types/sdk';
import { appschema } from './appschema';
import { copyAdaptiveAction } from './copyUtils';
@@ -11,11 +12,13 @@ interface DesignerAttributes {
description: string;
}
-export interface DesignerData {
- name?: string;
- description?: string;
- id: string;
-}
+const initialInputDialog = {
+ allowInterruptions: 'false',
+ prompt: '',
+ unrecognizedPrompt: '',
+ invalidPrompt: '',
+ defaultValueResponse: '',
+};
const initialDialogShape = {
'Microsoft.AdaptiveDialog': {
@@ -33,24 +36,15 @@ const initialDialogShape = {
$type: 'Microsoft.OnConversationUpdateActivity',
condition: "toLower(turn.Activity.membersAdded[0].name) != 'bot'",
},
- 'Microsoft.AttachmentInput': {
- allowInterruptions: 'false',
- },
- 'Microsoft.ChoiceInput': {
- allowInterruptions: 'false',
- },
- 'Microsoft.ConfirmInput': {
- allowInterruptions: 'false',
- },
- 'Microsoft.DateTimeInput': {
- allowInterruptions: 'false',
- },
- 'Microsoft.NumberInput': {
- allowInterruptions: 'false',
- },
- 'Microsoft.TextInput': {
- allowInterruptions: 'false',
+ 'Microsoft.SendActivity': {
+ activity: '',
},
+ 'Microsoft.AttachmentInput': initialInputDialog,
+ 'Microsoft.ChoiceInput': initialInputDialog,
+ 'Microsoft.ConfirmInput': initialInputDialog,
+ 'Microsoft.DateTimeInput': initialInputDialog,
+ 'Microsoft.NumberInput': initialInputDialog,
+ 'Microsoft.TextInput': initialInputDialog,
};
export function getNewDesigner(name: string, description: string) {
@@ -94,15 +88,14 @@ export const seedDefaults = (type: string) => {
return assignDefaults(properties);
};
-const updateDesigner = data => {
- const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', '');
- data.$designer = $designer;
-};
-
-// TODO: lgApi should also be included in shared lib instead of pass it in
-// since it's already used by Shell, Visual and Form.
-export const deepCopyAction = async (data, lgApi) => {
- return await copyAdaptiveAction(data, { lgApi, updateDesigner });
+export const deepCopyAction = async (
+ data,
+ copyLgTemplateToNewNode: (lgTemplateName: string, newNodeId: string) => Promise
+) => {
+ return await copyAdaptiveAction(data, {
+ getDesignerId,
+ copyLgTemplate: copyLgTemplateToNewNode,
+ });
};
export const seedNewDialog = (
diff --git a/Composer/packages/lib/shared/src/types/sdk.ts b/Composer/packages/lib/shared/src/types/sdk.ts
index 30aab1690d..9ed5cebb58 100644
--- a/Composer/packages/lib/shared/src/types/sdk.ts
+++ b/Composer/packages/lib/shared/src/types/sdk.ts
@@ -3,7 +3,13 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
-interface BaseSchema {
+export interface DesignerData {
+ name?: string;
+ description?: string;
+ id: string;
+}
+
+export interface BaseSchema {
/** Defines the valid properties for the component you are configuring (from a dialog .schema file) */
$type: string;
/** Inline id for reuse of an inline definition */
@@ -11,7 +17,7 @@ interface BaseSchema {
/** Copy the definition by id from a .dialog file. */
$copy?: string;
/** Extra information for the Bot Framework Composer. */
- $designer?: OpenObject;
+ $designer?: DesignerData;
}
/* Union of components which implement the IActivityTemplate interface */
@@ -59,6 +65,11 @@ export interface IRecognizerOption {
noValue?: boolean;
}
+/** Respond with an activity. */
+export interface SendActivity extends BaseSchema {
+ activity?: MicrosoftIActivityTemplate;
+}
+
/**
* Inputs
*/
@@ -229,6 +240,32 @@ export interface SwitchCondition extends BaseSchema {
default?: MicrosoftIDialog[];
}
+/** Two-way branch the conversation flow based on a condition. */
+export interface IfCondition extends BaseSchema {
+ /** Expression to evaluate. */
+ condition?: string;
+ actions?: MicrosoftIDialog[];
+ elseActions?: MicrosoftIDialog[];
+}
+
+/** Execute actions on each item in an a collection. */
+export interface Foreach extends BaseSchema {
+ itemsProperty?: string;
+ actions?: MicrosoftIDialog[];
+}
+
+/** Execute actions on each page (collection of items) in an array. */
+export interface ForeachPage extends BaseSchema {
+ itemsProperty?: string;
+ pageSize?: number;
+ actions?: MicrosoftIDialog[];
+}
+
+export interface EditActions extends BaseSchema {
+ changeType: string;
+ actions?: MicrosoftIDialog[];
+}
+
/** Flexible, data driven dialog that can adapt to the conversation. */
export interface MicrosoftAdaptiveDialog extends BaseSchema {
/** Optional dialog ID. */
@@ -250,4 +287,8 @@ export type MicrosoftIDialog =
| MicrosoftIRecognizer
| ITriggerCondition
| SwitchCondition
- | TextInput;
+ | TextInput
+ | SendActivity
+ | IfCondition
+ | Foreach
+ | ForeachPage;
diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts
index e420b21587..a694b24622 100644
--- a/Composer/packages/lib/shared/src/types/shell.ts
+++ b/Composer/packages/lib/shared/src/types/shell.ts
@@ -126,6 +126,7 @@ export interface ShellApi {
updateLuFile: (id: string, content: string) => Promise;
updateLgFile: (id: string, content: string) => Promise;
getLgTemplates: (id: string) => Promise;
+ copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise;
createLgTemplate: (id: string, template: LgTemplate, position: number) => Promise;
updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
removeLgTemplate: (id: string, templateName: string) => Promise;
diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts
index e21b8640db..7e682cc044 100644
--- a/Composer/packages/server/src/models/bot/botProject.ts
+++ b/Composer/packages/server/src/models/bot/botProject.ts
@@ -4,7 +4,7 @@
import fs from 'fs';
import isEqual from 'lodash/isEqual';
-import { FileInfo, DialogInfo, LgFile, LuFile } from '@bfc/shared';
+import { FileInfo, DialogInfo, LgFile, LuFile, getNewDesigner } from '@bfc/shared';
import { dialogIndexer, luIndexer, lgIndexer } from '@bfc/indexers';
import { Path } from '../../utility/path';
@@ -177,12 +177,22 @@ export class BotProject {
public updateBotInfo = async (name: string, description: string) => {
const dialogs = this.dialogs;
const mainDialog = dialogs.find(item => item.isRoot);
- if (mainDialog !== undefined) {
- mainDialog.content.$designer = {
- ...mainDialog.content.$designer,
- name,
- description,
- };
+
+ if (mainDialog && mainDialog.content) {
+ const oldDesigner = mainDialog.content.$designer;
+
+ let newDesigner;
+ if (oldDesigner && oldDesigner.id) {
+ newDesigner = {
+ ...oldDesigner,
+ name,
+ description,
+ };
+ } else {
+ newDesigner = getNewDesigner(name, description);
+ }
+
+ mainDialog.content.$designer = newDesigner;
await this.updateDialog('Main', mainDialog.content);
}
};