From 28c7599338bfd3803cd57e6bb2af80e70208b6ae Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 14:54:07 -0700 Subject: [PATCH 01/24] implement 'deepCopyAction' for copying action recursively --- .../visual-designer/src/utils/jsonTracker.ts | 4 +- Composer/packages/lib/shared/src/copyUtils.ts | 38 +++++++++++++++++++ .../packages/lib/shared/src/dialogFactory.ts | 30 +++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 Composer/packages/lib/shared/src/copyUtils.ts diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 3daae66cae..36dc83617a 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -1,5 +1,5 @@ import { cloneDeep, get, set } from 'lodash'; -import { seedNewDialog } from 'shared'; +import { seedNewDialog, deepCopyAction } from 'shared'; import { getFriendlyName } from '../components/nodes/utils'; @@ -170,7 +170,7 @@ export function insert(inputDialog, path, position, $type) { export function copyNodes(inputDialog, nodeIds: string[]): any[] { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - return JSON.parse(JSON.stringify(nodes)); + return nodes.map(x => deepCopyAction(x)); } export function cutNodes(inputDialog, nodeIds: string[]) { diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts new file mode 100644 index 0000000000..52df9b6551 --- /dev/null +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -0,0 +1,38 @@ +export 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. + */ +export function visitAdaptiveAction(input: any, visitor: (data: any) => any) { + if (!input || !input.$type) return; + + 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)) { + children.forEach(x => visitAdaptiveAction(x, visitor)); + } + } +} diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index baefe52b2f..dd825e56ae 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -1,6 +1,7 @@ import nanoid from 'nanoid/generate'; import { appschema } from './appschema'; +import { visitAdaptiveAction } from './copyUtils'; interface DesignerAttributes { name: string; @@ -74,17 +75,40 @@ export const seedDefaults = (type: string) => { const DEEP_COPY_TYPES = ['Microsoft.SendActivity']; export const needsDeepCopy = $type => { - return DEEP_COPY_TYPES.indexOf($type) !== -1; + return DEEP_COPY_TYPES.includes($type); }; -export const deepCopy: any = (_data, _lgApi) => { +export const deepCopyAction: any = (data, lgApi) => { // data.type is a SendActivity // data.id is bound to copied SendActivity // new id getDesignerId() // data.activity references an LG template // make new LG template based off of naming schema // assign to data.activity - return undefined; + if (!data || !data.$type) return {}; + + // Deep copy the original data. + const copy = JSON.parse(JSON.stringify(data)); + + // Copy the $designer part (if exists) or create new $designer part. + const overrideDesigner = data => { + const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', ''); + data.$designer = $designer; + }; + + // Copy specific parts an Adaptive action cares. + const overrideContent = data => { + if (data.$type === 'Microsoft.SendActivity') { + data.activity = 'I copied' + data.activity; + } + }; + + visitAdaptiveAction(copy, data => { + overrideDesigner(data); + overrideContent(data); + }); + + return copy; }; export const seedNewDialog = ( From 972e553aee194229971c357cebb6fe1695a7b8a1 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 15:22:24 -0700 Subject: [PATCH 02/24] implement async lg template copy --- .../visual-designer/src/editors/ObiEditor.tsx | 5 ++-- .../visual-designer/src/utils/jsonTracker.ts | 4 +-- Composer/packages/lib/shared/src/copyUtils.ts | 25 +++++++++++++++++-- .../packages/lib/shared/src/dialogFactory.ts | 12 ++++----- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 790a6513b8..8598dc4ac8 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -98,8 +98,9 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.CopySelection: handler = e => { - const copiedActions = copyNodes(data, e.actionIds); - clipboardContext.setClipboardActions(copiedActions); + copyNodes(data, e.actionIds).then(copiedActions => { + clipboardContext.setClipboardActions(copiedActions); + }); }; break; case NodeEventTypes.CutSelection: diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 36dc83617a..d301a5fd6f 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -168,9 +168,9 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -export function copyNodes(inputDialog, nodeIds: string[]): any[] { +export async function copyNodes(inputDialog, nodeIds: string[]): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - return nodes.map(x => deepCopyAction(x)); + return nodes.map(async x => await deepCopyAction(x)); } export function cutNodes(inputDialog, nodeIds: string[]) { diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 52df9b6551..9211e706bf 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -19,10 +19,10 @@ const childrenMap = { * @param {any} input The input Adaptive Action which has $type field. * @param {function} visitor The callback function called on each action node. */ -export function visitAdaptiveAction(input: any, visitor: (data: any) => any) { +export async function visitAdaptiveAction(input: any, visitor: (data: any) => Promise) { if (!input || !input.$type) return; - visitor(input); + await visitor(input); let childrenKeys = DEFAULT_CHILDREN_KEYS; if (input.$type && childrenMap[input.$type]) { @@ -36,3 +36,24 @@ export function visitAdaptiveAction(input: any, visitor: (data: any) => any) { } } } + +function isLgActivity(activity: string) { + return activity && activity.indexOf('bfdactivity-') !== -1; +} + +export async function copyLgActivity(activity: string, lgApi: any): Promise { + if (!activity) return ''; + if (!isLgActivity(activity) || !lgApi) return activity; + + const { getLgTemplates } = lgApi; + if (!getLgTemplates) return activity; + + let rawContent = ''; + try { + rawContent = await getLgTemplates('common', activity); + } catch (error) { + return activity; + } + + return rawContent; +} diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index dd825e56ae..795624e66b 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -1,7 +1,7 @@ import nanoid from 'nanoid/generate'; import { appschema } from './appschema'; -import { visitAdaptiveAction } from './copyUtils'; +import { visitAdaptiveAction, copyLgActivity } from './copyUtils'; interface DesignerAttributes { name: string; @@ -78,7 +78,7 @@ export const needsDeepCopy = $type => { return DEEP_COPY_TYPES.includes($type); }; -export const deepCopyAction: any = (data, lgApi) => { +export const deepCopyAction = async (data, lgApi) => { // data.type is a SendActivity // data.id is bound to copied SendActivity // new id getDesignerId() @@ -97,15 +97,15 @@ export const deepCopyAction: any = (data, lgApi) => { }; // Copy specific parts an Adaptive action cares. - const overrideContent = data => { + const overrideContent = async data => { if (data.$type === 'Microsoft.SendActivity') { - data.activity = 'I copied' + data.activity; + data.activity = await copyLgActivity(data.activity, lgApi); } }; - visitAdaptiveAction(copy, data => { + await visitAdaptiveAction(copy, async data => { overrideDesigner(data); - overrideContent(data); + await overrideContent(data); }); return copy; From 2217d4677d9d3d66a473ec74d7d8cb5b8f011628 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 15:27:29 -0700 Subject: [PATCH 03/24] inject lgApi into jsonTracker copy / cut --- .../visual-designer/src/editors/ObiEditor.tsx | 14 ++++++++------ .../visual-designer/src/utils/jsonTracker.ts | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 8598dc4ac8..5032138d34 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -37,12 +37,13 @@ export const ObiEditor: FC = ({ }): JSX.Element | null => { let divRef; - const { focusedId, focusedEvent, removeLgTemplate } = useContext(NodeRendererContext); + const { focusedId, focusedEvent, getLgTemplates, removeLgTemplate } = useContext(NodeRendererContext); const [clipboardContext, setClipboardContext] = useState({ clipboardActions: [], setClipboardActions: actions => setClipboardContext({ ...clipboardContext, clipboardActions: actions }), }); + const lgApi = { getLgTemplates, removeLgTemplate }; const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => { let handler; switch (eventName) { @@ -98,17 +99,18 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.CopySelection: handler = e => { - copyNodes(data, e.actionIds).then(copiedActions => { + copyNodes(data, e.actionIds, lgApi).then(copiedActions => { clipboardContext.setClipboardActions(copiedActions); }); }; break; case NodeEventTypes.CutSelection: handler = e => { - const { dialog, cutData } = cutNodes(data, e.actionIds); - clipboardContext.setClipboardActions(cutData); - onChange(dialog); - onFocusSteps([]); + cutNodes(data, e.actionIds, lgApi).then(({ dialog, cutData }) => { + clipboardContext.setClipboardActions(cutData); + onChange(dialog); + onFocusSteps([]); + }); }; break; case NodeEventTypes.DeleteSelection: diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index d301a5fd6f..34ee6f913b 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -168,13 +168,13 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -export async function copyNodes(inputDialog, nodeIds: string[]): Promise { +export async function copyNodes(inputDialog, nodeIds: string[], lgApi): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - return nodes.map(async x => await deepCopyAction(x)); + return nodes.map(async x => await deepCopyAction(x, lgApi)); } -export function cutNodes(inputDialog, nodeIds: string[]) { - const nodesData = copyNodes(inputDialog, nodeIds); +export async function cutNodes(inputDialog, nodeIds: string[], lgApi) { + const nodesData = await copyNodes(inputDialog, nodeIds, lgApi); const newDialog = deleteNodes(inputDialog, nodeIds); return { dialog: newDialog, cutData: nodesData }; From a47d52e47adb5f7a6bb29f2ca2ab8e67a9e56a90 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 15:30:12 -0700 Subject: [PATCH 04/24] add mock lgApi in demo --- .../visual-designer/demo/src/stories/VisualEditorDemo.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js index 40ecca98f7..cab9acab2f 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js @@ -103,6 +103,12 @@ export class VisualEditorDemo extends Component { obiJson: json, }); }, + getLgTemplates: () => { + return Promise.resolve('LgTemplate Placeholder.'); + }, + removeLgTemplate: () => { + return Promise.resolve(true); + }, }} /> From 3eebc552be27d270ff68ceac99bed2742eeb6c73 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 16:17:10 -0700 Subject: [PATCH 05/24] make LgApi promise work --- .../visual-designer/demo/src/stories/VisualEditorDemo.js | 2 +- .../extensions/visual-designer/src/utils/jsonTracker.ts | 8 +++++++- Composer/packages/lib/shared/src/copyUtils.ts | 9 ++++++--- Composer/packages/lib/shared/src/dialogFactory.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js index cab9acab2f..07a8277bb9 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js @@ -104,7 +104,7 @@ export class VisualEditorDemo extends Component { }); }, getLgTemplates: () => { - return Promise.resolve('LgTemplate Placeholder.'); + return Promise.resolve([{ Name: 'lg', Body: 'LgTemplate Placeholder.' }]); }, removeLgTemplate: () => { return Promise.resolve(true); diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 34ee6f913b..d9a6661c7c 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -170,7 +170,13 @@ export function insert(inputDialog, path, position, $type) { export async function copyNodes(inputDialog, nodeIds: string[], lgApi): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - return nodes.map(async x => await deepCopyAction(x, lgApi)); + const copiedNodes = await Promise.all( + nodes.map(async x => { + const node = await deepCopyAction(x, lgApi); + return node; + }) + ); + return copiedNodes; } export async function cutNodes(inputDialog, nodeIds: string[], lgApi) { diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 9211e706bf..08e5d61a8d 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -48,12 +48,15 @@ export async function copyLgActivity(activity: string, lgApi: any): Promise `[${lg.Name}]` === activity); + + if (currentLg) return currentLg.Body; + return activity; } diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 795624e66b..2d9bc32da4 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -44,8 +44,8 @@ export function getNewDesigner(name: string, description: string) { export const getDesignerId = (data?: DesignerData) => { const newDesigner: DesignerData = { - id: nanoid('1234567890', 6), ...data, + id: nanoid('1234567890', 6), }; return newDesigner; From e6e526869c1dcc899846d7a38831792cd3dcb2ff Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 11 Oct 2019 23:30:25 -0700 Subject: [PATCH 06/24] fix two unstable CI spec (why it started to failed here? it should have been failed in very early PR since we no longer focus to default event) --- Composer/cypress/integration/Breadcrumb.spec.js | 3 +++ .../cypress/integration/VisualDesigner.spec.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js index 9eaf3a7749..2436304459 100644 --- a/Composer/cypress/integration/Breadcrumb.spec.js +++ b/Composer/cypress/integration/Breadcrumb.spec.js @@ -4,12 +4,14 @@ context('breadcrumb', () => { before(() => { cy.visit(Cypress.env('COMPOSER_URL')); cy.openBot('ToDoBot'); + cy.wait(100); }); beforeEach(() => { // Return to Main.dialog cy.get('[data-testid="ProjectTree"]').within(() => { cy.getByText('ToDoBot.Main').click(); + cy.wait(100); }); }); @@ -30,6 +32,7 @@ context('breadcrumb', () => { // Return to Main.dialog cy.get('[data-testid="ProjectTree"]').within(() => { cy.getByText('ToDoBot.Main').click(); + cy.wait(100); }); cy.getByTestId('Breadcrumb') diff --git a/Composer/cypress/integration/VisualDesigner.spec.js b/Composer/cypress/integration/VisualDesigner.spec.js index e0b5b8c8bc..2aa9c78579 100644 --- a/Composer/cypress/integration/VisualDesigner.spec.js +++ b/Composer/cypress/integration/VisualDesigner.spec.js @@ -4,9 +4,23 @@ context('Visual Designer', () => { before(() => { cy.visit(Cypress.env('COMPOSER_URL')); cy.openBot('ToDoBot'); + cy.wait(100); + }); + + beforeEach(() => { + // Return to Main.dialog + cy.get('[data-testid="ProjectTree"]').within(() => { + cy.getByText('ToDoBot.Main').click(); + cy.wait(100); + }); }); it('can find Visual Designer default trigger in container', () => { + cy.get('[data-testid="ProjectTree"]').within(() => { + cy.getByText('Handle ConversationUpdate').click(); + cy.wait(500); + }); + cy.withinEditor('VisualEditor', () => { cy.getByText('Trigger').should('exist'); }); From 43724000b0c26c8dd48e94536904e250ea9c944a Mon Sep 17 00:00:00 2001 From: zeye Date: Mon, 14 Oct 2019 09:20:25 -0700 Subject: [PATCH 07/24] increase Breadcrumb spec waiting time to stablize --- Composer/cypress/integration/Breadcrumb.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js index 2436304459..6cec007998 100644 --- a/Composer/cypress/integration/Breadcrumb.spec.js +++ b/Composer/cypress/integration/Breadcrumb.spec.js @@ -10,8 +10,8 @@ context('breadcrumb', () => { beforeEach(() => { // Return to Main.dialog cy.get('[data-testid="ProjectTree"]').within(() => { + cy.wait(1000); cy.getByText('ToDoBot.Main').click(); - cy.wait(100); }); }); From 0ab66e054727b16adbf8e800213a3d4d9fcfe4ce Mon Sep 17 00:00:00 2001 From: zeye Date: Mon, 14 Oct 2019 09:52:05 -0700 Subject: [PATCH 08/24] fix CI by adding wait() --- Composer/cypress/integration/Breadcrumb.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js index 6cec007998..c2955b8b7c 100644 --- a/Composer/cypress/integration/Breadcrumb.spec.js +++ b/Composer/cypress/integration/Breadcrumb.spec.js @@ -12,6 +12,7 @@ context('breadcrumb', () => { cy.get('[data-testid="ProjectTree"]').within(() => { cy.wait(1000); cy.getByText('ToDoBot.Main').click(); + cy.wait(1000); }); }); From c6c7a917421c986918f22cb50d26e2c1d60c9788 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 15 Oct 2019 11:18:38 -0700 Subject: [PATCH 09/24] remove comments --- Composer/packages/lib/shared/src/dialogFactory.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 2d9bc32da4..25233676e4 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -79,12 +79,6 @@ export const needsDeepCopy = $type => { }; export const deepCopyAction = async (data, lgApi) => { - // data.type is a SendActivity - // data.id is bound to copied SendActivity - // new id getDesignerId() - // data.activity references an LG template - // make new LG template based off of naming schema - // assign to data.activity if (!data || !data.$type) return {}; // Deep copy the original data. From 84c954b1200ea1f3a559e57094c8a20d4849edd9 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 15 Oct 2019 11:20:27 -0700 Subject: [PATCH 10/24] visit -> walk --- Composer/packages/lib/shared/src/dialogFactory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 25233676e4..0a64284f36 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -1,7 +1,7 @@ import nanoid from 'nanoid/generate'; import { appschema } from './appschema'; -import { visitAdaptiveAction, copyLgActivity } from './copyUtils'; +import { visitAdaptiveAction as walkAdaptiveAction, copyLgActivity } from './copyUtils'; interface DesignerAttributes { name: string; @@ -97,7 +97,7 @@ export const deepCopyAction = async (data, lgApi) => { } }; - await visitAdaptiveAction(copy, async data => { + await walkAdaptiveAction(copy, async data => { overrideDesigner(data); await overrideContent(data); }); From 0d25c599129742b83c83738f100e027f00b2575c Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 15 Oct 2019 12:20:41 -0700 Subject: [PATCH 11/24] apply ''needsDeepCopy" --- Composer/packages/lib/shared/src/dialogFactory.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 0a64284f36..f6828b0578 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -84,13 +84,12 @@ export const deepCopyAction = async (data, lgApi) => { // Deep copy the original data. const copy = JSON.parse(JSON.stringify(data)); - // Copy the $designer part (if exists) or create new $designer part. - const overrideDesigner = data => { + const updateDesigner = data => { const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', ''); data.$designer = $designer; }; - // Copy specific parts an Adaptive action cares. + // Copy raw LG activity. const overrideContent = async data => { if (data.$type === 'Microsoft.SendActivity') { data.activity = await copyLgActivity(data.activity, lgApi); @@ -98,8 +97,10 @@ export const deepCopyAction = async (data, lgApi) => { }; await walkAdaptiveAction(copy, async data => { - overrideDesigner(data); - await overrideContent(data); + updateDesigner(data); + if (needsDeepCopy(data.$type)) { + await overrideContent(data); + } }); return copy; From 29ab0c4caf8a8fa6df516bfda5e0b2b0636a67bd Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 15 Oct 2019 16:34:21 -0700 Subject: [PATCH 12/24] Fix lint error --- Composer/packages/lib/shared/src/copyUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 08e5d61a8d..2bc7cd4215 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -32,13 +32,13 @@ export async function visitAdaptiveAction(input: any, visitor: (data: any) => Pr for (const childrenKey of childrenKeys) { const children = input[childrenKey]; if (Array.isArray(children)) { - children.forEach(x => visitAdaptiveAction(x, visitor)); + Promise.all(children.map(async x => await visitAdaptiveAction(x, visitor))); } } } function isLgActivity(activity: string) { - return activity && activity.indexOf('bfdactivity-') !== -1; + return activity && activity.includes('bfdactivity-'); } export async function copyLgActivity(activity: string, lgApi: any): Promise { From 500be041c7ca8e8e9d9625c2b1157527561da892 Mon Sep 17 00:00:00 2001 From: zeye Date: Wed, 16 Oct 2019 11:39:53 -0700 Subject: [PATCH 13/24] move all copy logic into copyUtils --- Composer/packages/lib/shared/src/copyUtils.ts | 42 +++++++++++++++++-- .../packages/lib/shared/src/dialogFactory.ts | 35 ++++------------ 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 2bc7cd4215..197971fd5e 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -19,7 +19,7 @@ const childrenMap = { * @param {any} input The input Adaptive Action which has $type field. * @param {function} visitor The callback function called on each action node. */ -export async function visitAdaptiveAction(input: any, visitor: (data: any) => Promise) { +async function walkAdaptiveAction(input: any, visitor: (data: any) => Promise) { if (!input || !input.$type) return; await visitor(input); @@ -32,7 +32,7 @@ export async function visitAdaptiveAction(input: any, visitor: (data: any) => Pr for (const childrenKey of childrenKeys) { const children = input[childrenKey]; if (Array.isArray(children)) { - Promise.all(children.map(async x => await visitAdaptiveAction(x, visitor))); + Promise.all(children.map(async x => await walkAdaptiveAction(x, visitor))); } } } @@ -41,7 +41,7 @@ function isLgActivity(activity: string) { return activity && activity.includes('bfdactivity-'); } -export async function copyLgActivity(activity: string, lgApi: any): Promise { +async function copyLgActivity(activity: string, lgApi: any): Promise { if (!activity) return ''; if (!isLgActivity(activity) || !lgApi) return activity; @@ -60,3 +60,39 @@ export async function copyLgActivity(activity: string, lgApi: any): Promise any = updateDesigner; + + if (needsDeepCopy(data.$type)) { + const advancedCopyHandler = async data => { + updateDesigner(data); + await overrideExternalReferences(data, { lgApi }); + }; + copyHandler = advancedCopyHandler; + } + + // Walk action and rewrite needs copy fields + await walkAdaptiveAction(copy, copyHandler); + + return copy; +} diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index f6828b0578..7dc9b404de 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -1,7 +1,7 @@ import nanoid from 'nanoid/generate'; import { appschema } from './appschema'; -import { visitAdaptiveAction as walkAdaptiveAction, copyLgActivity } from './copyUtils'; +import { copyAdaptiveAction } from './copyUtils'; interface DesignerAttributes { name: string; @@ -78,32 +78,15 @@ export const needsDeepCopy = $type => { return DEEP_COPY_TYPES.includes($type); }; -export const deepCopyAction = async (data, lgApi) => { - if (!data || !data.$type) return {}; - - // Deep copy the original data. - const copy = JSON.parse(JSON.stringify(data)); - - const updateDesigner = data => { - const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', ''); - data.$designer = $designer; - }; - - // Copy raw LG activity. - const overrideContent = async data => { - if (data.$type === 'Microsoft.SendActivity') { - data.activity = await copyLgActivity(data.activity, lgApi); - } - }; - - await walkAdaptiveAction(copy, async data => { - updateDesigner(data); - if (needsDeepCopy(data.$type)) { - await overrideContent(data); - } - }); +const updateDesigner = data => { + const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', ''); + data.$designer = $designer; +}; - return copy; +// 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, needsDeepCopy, updateDesigner }); }; export const seedNewDialog = ( From 594d1eddd85473f2e51ae603994b87e43d3ebf4f Mon Sep 17 00:00:00 2001 From: zeye Date: Wed, 16 Oct 2019 20:10:53 -0700 Subject: [PATCH 14/24] use a function map to handle different types overrider --- Composer/packages/lib/shared/src/copyUtils.ts | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 197971fd5e..29d6b9bdf3 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -38,7 +38,7 @@ async function walkAdaptiveAction(input: any, visitor: (data: any) => Promise { @@ -61,21 +61,40 @@ async function copyLgActivity(activity: string, lgApi: any): Promise { return activity; } -async function overrideExternalReferences(data, externalApi) { - const { lgApi } = externalApi; +// TODO: use $type from SDKTypes (after solving circular import issue). +const OverriderByType = { + 'Microsoft.SendActivity': async (data, { lgApi }) => { + data.activity = await copyLgActivity(data.activity, lgApi); + }, + 'Microsoft.AttachmentInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.ConfirmInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.DateTimeInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.NumberInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.OAuthInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.TextInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, + 'Microsoft.ChoiceInput': async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); + }, +}; - // Override specific fields different actions care. - switch (data.$type) { - case 'Microsoft.SendActivity': - data.activity = await copyLgActivity(data.activity, lgApi); - break; - } -} +const needsDeepCopy = data => !!(data && OverriderByType[data.$type]); export async function copyAdaptiveAction(data, externalApi) { if (!data || !data.$type) return {}; - const { lgApi, updateDesigner, needsDeepCopy } = externalApi; + const { updateDesigner } = externalApi; // Deep copy the original data. const copy = JSON.parse(JSON.stringify(data)); @@ -86,7 +105,10 @@ export async function copyAdaptiveAction(data, externalApi) { if (needsDeepCopy(data.$type)) { const advancedCopyHandler = async data => { updateDesigner(data); - await overrideExternalReferences(data, { lgApi }); + if (OverriderByType[data.$type]) { + const overrider = OverriderByType[data.$type]; + await overrider(data, externalApi); + } }; copyHandler = advancedCopyHandler; } From b92aebc744a95eb0c51e6aa1253f6bf7450ce414 Mon Sep 17 00:00:00 2001 From: zeye Date: Wed, 16 Oct 2019 20:22:42 -0700 Subject: [PATCH 15/24] simplify the overrider map --- Composer/packages/lib/shared/src/copyUtils.ts | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 29d6b9bdf3..dd0641b5bb 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -61,32 +61,24 @@ async function copyLgActivity(activity: string, lgApi: any): Promise { return activity; } +const overrideLgActivity = async (data, { lgApi }) => { + data.activity = await copyLgActivity(data.activity, lgApi); +}; + +const overrideLgPrompt = async (data, { lgApi }) => { + data.prompt = await copyLgActivity(data.prompt, lgApi); +}; + // TODO: use $type from SDKTypes (after solving circular import issue). const OverriderByType = { - 'Microsoft.SendActivity': async (data, { lgApi }) => { - data.activity = await copyLgActivity(data.activity, lgApi); - }, - 'Microsoft.AttachmentInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.ConfirmInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.DateTimeInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.NumberInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.OAuthInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.TextInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, - 'Microsoft.ChoiceInput': async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); - }, + '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 needsDeepCopy = data => !!(data && OverriderByType[data.$type]); @@ -102,7 +94,7 @@ export async function copyAdaptiveAction(data, externalApi) { // Create copy handler for rewriting fields which need to be handled specially. let copyHandler: (data) => any = updateDesigner; - if (needsDeepCopy(data.$type)) { + if (needsDeepCopy(data)) { const advancedCopyHandler = async data => { updateDesigner(data); if (OverriderByType[data.$type]) { From 72fbe49e5896cc589fb0f357ed77b75e78dc91c5 Mon Sep 17 00:00:00 2001 From: zeye Date: Wed, 16 Oct 2019 20:24:47 -0700 Subject: [PATCH 16/24] remove needsDeepCopy handler outside --- Composer/packages/lib/shared/src/dialogFactory.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 7dc9b404de..5cf655efff 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -73,11 +73,6 @@ export const seedDefaults = (type: string) => { return assignDefaults(properties); }; -const DEEP_COPY_TYPES = ['Microsoft.SendActivity']; -export const needsDeepCopy = $type => { - return DEEP_COPY_TYPES.includes($type); -}; - const updateDesigner = data => { const $designer = data.$designer ? getDesignerId(data.$designer) : getNewDesigner('', ''); data.$designer = $designer; @@ -86,7 +81,7 @@ const updateDesigner = data => { // 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, needsDeepCopy, updateDesigner }); + return await copyAdaptiveAction(data, { lgApi, updateDesigner }); }; export const seedNewDialog = ( From 9ed7aa60e77568b5d96759e3aaf176cabe9f59fa Mon Sep 17 00:00:00 2001 From: zeye Date: Wed, 16 Oct 2019 20:30:15 -0700 Subject: [PATCH 17/24] judge needsOverride --- Composer/packages/lib/shared/src/copyUtils.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index dd0641b5bb..49150a4a18 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -81,7 +81,7 @@ const OverriderByType = { 'Microsoft.ChoiceInput': overrideLgPrompt, }; -const needsDeepCopy = data => !!(data && OverriderByType[data.$type]); +const needsOverride = data => !!(data && OverriderByType[data.$type]); export async function copyAdaptiveAction(data, externalApi) { if (!data || !data.$type) return {}; @@ -92,18 +92,13 @@ export async function copyAdaptiveAction(data, externalApi) { const copy = JSON.parse(JSON.stringify(data)); // Create copy handler for rewriting fields which need to be handled specially. - let copyHandler: (data) => any = updateDesigner; - - if (needsDeepCopy(data)) { - const advancedCopyHandler = async data => { - updateDesigner(data); - if (OverriderByType[data.$type]) { - const overrider = OverriderByType[data.$type]; - await overrider(data, externalApi); - } - }; - copyHandler = advancedCopyHandler; - } + 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); From 026000bf56175bf5dfea9da2dee57234f6c15558 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Thu, 17 Oct 2019 18:29:31 +0800 Subject: [PATCH 18/24] delete prefix for coped node by adding new lg template for copied node --- .../demo/src/stories/VisualEditorDemo.js | 3 +++ .../visual-designer/src/editors/ObiEditor.tsx | 6 ++++-- .../packages/extensions/visual-designer/src/index.tsx | 2 ++ .../visual-designer/src/store/NodeRendererContext.ts | 2 ++ Composer/packages/lib/shared/src/copyUtils.ts | 11 +++++++---- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js index 19cc9ac9b7..d99810295d 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js @@ -107,6 +107,9 @@ export class VisualEditorDemo extends Component { obiJson: json, }); }, + updateLgTemplate: ({ id, templateName, template }) => { + return Promise.resolve(''); + }, getLgTemplates: () => { return Promise.resolve([{ Name: 'lg', Body: 'LgTemplate Placeholder.' }]); }, diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index d90c845396..e06536141d 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -39,13 +39,15 @@ export const ObiEditor: FC = ({ }): JSX.Element | null => { let divRef; - const { focusedId, focusedEvent, getLgTemplates, removeLgTemplate } = useContext(NodeRendererContext); + const { focusedId, focusedEvent, updateLgTemplate, getLgTemplates, removeLgTemplate } = useContext( + NodeRendererContext + ); const [clipboardContext, setClipboardContext] = useState({ clipboardActions: [], setClipboardActions: actions => setClipboardContext({ ...clipboardContext, clipboardActions: actions }), }); - const lgApi = { getLgTemplates, removeLgTemplate }; + const lgApi = { getLgTemplates, removeLgTemplate, updateLgTemplate }; const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => { let handler; switch (eventName) { diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx index a78154826f..f1c710e5d8 100644 --- a/Composer/packages/extensions/visual-designer/src/index.tsx +++ b/Composer/packages/extensions/visual-designer/src/index.tsx @@ -37,6 +37,7 @@ const VisualDesigner: React.FC = ({ onFocusSteps, onSelect, saveData, + updateLgTemplate, getLgTemplates, removeLgTemplate, undo, @@ -50,6 +51,7 @@ const VisualDesigner: React.FC = ({ focusedId, focusedEvent, focusedTab, + updateLgTemplate: updateLgTemplate, getLgTemplates: getLgTemplates, removeLgTemplate: removeLgTemplate, }); diff --git a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts index eed3c88213..384a9d5b2d 100644 --- a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts +++ b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { string } from 'prop-types'; interface LgTemplate { Name: string; @@ -11,4 +12,5 @@ export const NodeRendererContext = React.createContext({ focusedTab: '', getLgTemplates: (_id: string, _templateName: string) => Promise.resolve([] as LgTemplate[]), removeLgTemplate: (_id: string, _templateName: string) => Promise.resolve(), + updateLgTemplate: (_id: string, _templateName: string, _template: string) => Promise.resolve('' as string), }); diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 197971fd5e..202f23037b 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -41,11 +41,11 @@ function isLgActivity(activity: string) { return activity && activity.includes('bfdactivity-'); } -async function copyLgActivity(activity: string, lgApi: any): Promise { +async function copyLgActivity(activity: string, id: string, lgApi: any): Promise { if (!activity) return ''; if (!isLgActivity(activity) || !lgApi) return activity; - const { getLgTemplates } = lgApi; + const { getLgTemplates, updateLgTemplate } = lgApi; if (!getLgTemplates) return activity; let rawLg: any[] = []; @@ -57,7 +57,10 @@ async function copyLgActivity(activity: string, lgApi: any): Promise { const currentLg = rawLg.find(lg => `[${lg.Name}]` === activity); - if (currentLg) return currentLg.Body; + if (currentLg) { + await updateLgTemplate('common', `bfdactivity-${id}`, currentLg.Body); + return `[bfdactivity-${id}]`; + } return activity; } @@ -67,7 +70,7 @@ async function overrideExternalReferences(data, externalApi) { // Override specific fields different actions care. switch (data.$type) { case 'Microsoft.SendActivity': - data.activity = await copyLgActivity(data.activity, lgApi); + data.activity = await copyLgActivity(data.activity, data.$designer.id, lgApi); break; } } From dda7fb112f8df57a25537bcedf2043abb3b0e799 Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 17 Oct 2019 08:27:31 -0700 Subject: [PATCH 19/24] revert changes in copyUtils --- Composer/packages/lib/shared/src/copyUtils.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 202f23037b..197971fd5e 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -41,11 +41,11 @@ function isLgActivity(activity: string) { return activity && activity.includes('bfdactivity-'); } -async function copyLgActivity(activity: string, id: string, lgApi: any): Promise { +async function copyLgActivity(activity: string, lgApi: any): Promise { if (!activity) return ''; if (!isLgActivity(activity) || !lgApi) return activity; - const { getLgTemplates, updateLgTemplate } = lgApi; + const { getLgTemplates } = lgApi; if (!getLgTemplates) return activity; let rawLg: any[] = []; @@ -57,10 +57,7 @@ async function copyLgActivity(activity: string, id: string, lgApi: any): Promise const currentLg = rawLg.find(lg => `[${lg.Name}]` === activity); - if (currentLg) { - await updateLgTemplate('common', `bfdactivity-${id}`, currentLg.Body); - return `[bfdactivity-${id}]`; - } + if (currentLg) return currentLg.Body; return activity; } @@ -70,7 +67,7 @@ async function overrideExternalReferences(data, externalApi) { // Override specific fields different actions care. switch (data.$type) { case 'Microsoft.SendActivity': - data.activity = await copyLgActivity(data.activity, data.$designer.id, lgApi); + data.activity = await copyLgActivity(data.activity, lgApi); break; } } From 35adcdfca6d5876ab968809861e9714f4a506f69 Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 17 Oct 2019 08:34:38 -0700 Subject: [PATCH 20/24] remove unnecessary export --- Composer/packages/lib/shared/src/copyUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 49150a4a18..75a4f7e198 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -1,4 +1,4 @@ -export const NestedFieldNames = { +const NestedFieldNames = { Actions: 'actions', ElseActions: 'elseActions', DefaultCase: 'default', @@ -86,11 +86,10 @@ const needsOverride = data => !!(data && OverriderByType[data.$type]); export async function copyAdaptiveAction(data, externalApi) { if (!data || !data.$type) return {}; - const { updateDesigner } = externalApi; - // 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); From 4b368dbe8b85627a2bbb2b27b88920a54056bced Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 17 Oct 2019 08:56:40 -0700 Subject: [PATCH 21/24] deep copy node at 'paste' stage instead of 'copy' --- .../visual-designer/src/editors/ObiEditor.tsx | 9 ++++--- .../visual-designer/src/utils/jsonTracker.ts | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index e06536141d..887d619016 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -83,8 +83,9 @@ export const ObiEditor: FC = ({ case NodeEventTypes.Insert: if (eventData.$type === 'PASTE') { handler = e => { - const dialog = pasteNodes(data, e.id, e.position, clipboardContext.clipboardActions); - onChange(dialog); + pasteNodes(data, e.id, e.position, clipboardContext.clipboardActions, lgApi).then(dialog => { + onChange(dialog); + }); }; } else { handler = e => { @@ -103,14 +104,14 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.CopySelection: handler = e => { - copyNodes(data, e.actionIds, lgApi).then(copiedActions => { + copyNodes(data, e.actionIds).then(copiedActions => { clipboardContext.setClipboardActions(copiedActions); }); }; break; case NodeEventTypes.CutSelection: handler = e => { - cutNodes(data, e.actionIds, lgApi).then(({ dialog, cutData }) => { + cutNodes(data, e.actionIds).then(({ dialog, cutData }) => { clipboardContext.setClipboardActions(cutData); onChange(dialog); onFocusSteps([]); diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index e88289c78b..0ab41ee78d 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -168,19 +168,13 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -export async function copyNodes(inputDialog, nodeIds: string[], lgApi): Promise { +export async function copyNodes(inputDialog, nodeIds: string[]): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - const copiedNodes = await Promise.all( - nodes.map(async x => { - const node = await deepCopyAction(x, lgApi); - return node; - }) - ); - return copiedNodes; + return JSON.parse(JSON.stringify(nodes)); } -export async function cutNodes(inputDialog, nodeIds: string[], lgApi) { - const nodesData = await copyNodes(inputDialog, nodeIds, lgApi); +export async function cutNodes(inputDialog, nodeIds: string[]) { + const nodesData = await copyNodes(inputDialog, nodeIds); const newDialog = deleteNodes(inputDialog, nodeIds); return { dialog: newDialog, cutData: nodesData }; @@ -202,7 +196,7 @@ export function appendNodesAfter(inputDialog, targetId, newNodes) { return dialog; } -export function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes) { +export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, lgApi) { if (!Array.isArray(newNodes) || newNodes.length === 0) { return inputDialog; } @@ -214,6 +208,13 @@ export function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes) { return inputDialog; } - targetArray.currentData.splice(arrayIndex, 0, ...newNodes); + // Deep copy nodes with external resources + const copiedNodes = await Promise.all( + newNodes.map(async x => { + const node = await deepCopyAction(x, lgApi); + return node; + }) + ); + targetArray.currentData.splice(arrayIndex, 0, ...copiedNodes); return dialog; } From 7f8b749a705121464993d40ca50baf90fab2b188 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 18 Oct 2019 00:11:11 +0800 Subject: [PATCH 22/24] create new lg at 'paste' stage --- Composer/packages/lib/shared/src/copyUtils.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 75a4f7e198..0f2d02d7a2 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -41,11 +41,11 @@ function isLgActivity(activity: string) { return activity && (activity.includes('bfdactivity-') || activity.includes('bfdprompt-')); } -async function copyLgActivity(activity: string, lgApi: any): Promise { +async function copyLgActivity(activity: string, designerId: string, lgApi: any): Promise { if (!activity) return ''; if (!isLgActivity(activity) || !lgApi) return activity; - const { getLgTemplates } = lgApi; + const { getLgTemplates, updateLgTemplate } = lgApi; if (!getLgTemplates) return activity; let rawLg: any[] = []; @@ -57,16 +57,22 @@ async function copyLgActivity(activity: string, lgApi: any): Promise { const currentLg = rawLg.find(lg => `[${lg.Name}]` === activity); - if (currentLg) return currentLg.Body; + if (currentLg) { + // Create new lg activity. + const newLgContent = currentLg.Body; + const newLgId = `bfdactivity-${designerId}`; + await updateLgTemplate('common', newLgId, newLgContent); + return `[${newLgId}]`; + } return activity; } const overrideLgActivity = async (data, { lgApi }) => { - data.activity = await copyLgActivity(data.activity, lgApi); + data.activity = await copyLgActivity(data.activity, data.$designer.id, lgApi); }; const overrideLgPrompt = async (data, { lgApi }) => { - data.prompt = await copyLgActivity(data.prompt, lgApi); + data.prompt = await copyLgActivity(data.prompt, data.$designer.id, lgApi); }; // TODO: use $type from SDKTypes (after solving circular import issue). From df52ffa98e9e7aad9057f55da18dc5766053bd03 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Fri, 18 Oct 2019 11:09:36 +0800 Subject: [PATCH 23/24] eslint --- .../visual-designer/src/editors/ObiEditor.tsx | 14 ++++++-------- .../visual-designer/src/utils/jsonTracker.ts | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 887d619016..674175de5a 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -104,18 +104,16 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.CopySelection: handler = e => { - copyNodes(data, e.actionIds).then(copiedActions => { - clipboardContext.setClipboardActions(copiedActions); - }); + const copiedActions = copyNodes(data, e.actionIds); + clipboardContext.setClipboardActions(copiedActions); }; break; case NodeEventTypes.CutSelection: handler = e => { - cutNodes(data, e.actionIds).then(({ dialog, cutData }) => { - clipboardContext.setClipboardActions(cutData); - onChange(dialog); - onFocusSteps([]); - }); + const { dialog, cutData } = cutNodes(data, e.actionIds); + clipboardContext.setClipboardActions(cutData); + onChange(dialog); + onFocusSteps([]); }; break; case NodeEventTypes.DeleteSelection: diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 0ab41ee78d..842216468b 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -168,13 +168,13 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -export async function copyNodes(inputDialog, nodeIds: string[]): Promise { +export function copyNodes(inputDialog, nodeIds: string[]): any[] { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); return JSON.parse(JSON.stringify(nodes)); } -export async function cutNodes(inputDialog, nodeIds: string[]) { - const nodesData = await copyNodes(inputDialog, nodeIds); +export function cutNodes(inputDialog, nodeIds: string[]) { + const nodesData = copyNodes(inputDialog, nodeIds); const newDialog = deleteNodes(inputDialog, nodeIds); return { dialog: newDialog, cutData: nodesData }; From e6a213082a5c0d218da1d5dd39ec3f8197455ca2 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 18 Oct 2019 15:58:52 +0800 Subject: [PATCH 24/24] add unit tests for copyUtils --- .../lib/shared/__tests__/copyUtils.test.ts | 102 ++++++++++++++++++ Composer/packages/lib/shared/src/copyUtils.ts | 8 +- 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 Composer/packages/lib/shared/__tests__/copyUtils.test.ts diff --git a/Composer/packages/lib/shared/__tests__/copyUtils.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils.test.ts new file mode 100644 index 0000000000..5938033901 --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/copyUtils.test.ts @@ -0,0 +1,102 @@ +import { copyAdaptiveAction } from '../src/copyUtils'; + +describe('copyAdaptiveAction', () => { + const lgTemplate = [{ Name: 'bfdactivity-1234', Body: '-hello' }]; + 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: '[bfdactivity-1234]', + }; + + expect(await copyAdaptiveAction(promptText, externalApi)).toEqual({ + $type: 'Microsoft.TextInput', + $designer: { + id: '5678', + }, + maxTurnCount: 3, + alwaysPrompt: false, + allowInterruptions: 'true', + outputFormat: 'none', + prompt: '[bfdactivity-5678]', + }); + + expect(await copyAdaptiveAction(promptText, externalApiWithFailure)).toEqual({ + $type: 'Microsoft.TextInput', + $designer: { + id: '5678', + }, + maxTurnCount: 3, + alwaysPrompt: false, + allowInterruptions: 'true', + outputFormat: 'none', + prompt: '-hello', + }); + }); +}); diff --git a/Composer/packages/lib/shared/src/copyUtils.ts b/Composer/packages/lib/shared/src/copyUtils.ts index 0f2d02d7a2..f65e59144d 100644 --- a/Composer/packages/lib/shared/src/copyUtils.ts +++ b/Composer/packages/lib/shared/src/copyUtils.ts @@ -61,8 +61,12 @@ async function copyLgActivity(activity: string, designerId: string, lgApi: any): // Create new lg activity. const newLgContent = currentLg.Body; const newLgId = `bfdactivity-${designerId}`; - await updateLgTemplate('common', newLgId, newLgContent); - return `[${newLgId}]`; + try { + await updateLgTemplate('common', newLgId, newLgContent); + return `[${newLgId}]`; + } catch (e) { + return newLgContent; + } } return activity; }