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); } };