From 495150cef8d261fd15e8b1b6aeebcaea5ab2a405 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Wed, 11 Mar 2020 20:36:15 +0800 Subject: [PATCH 1/8] dump real lg content before paste them --- .../visual-designer/src/editors/ObiEditor.tsx | 50 +++++++++---------- .../visual-designer/src/utils/jsonTracker.ts | 47 +++++++++-------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 8c1c35a255..6357a1d73b 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,7 +5,7 @@ import { jsx } from '@emotion/core'; import { useContext, FC, useEffect, useState, useRef } from 'react'; import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { deleteAction, deleteActions, LgTemplateRef, LgMetaData } from '@bfc/shared'; +import { deleteAction, deleteActions, LgTemplateRef } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -44,10 +44,21 @@ export const ObiEditor: FC = ({ }): JSX.Element | null => { let divRef; - const { focusedId, focusedEvent, clipboardActions, copyLgTemplate, removeLgTemplates, removeLuIntent } = useContext( + const { focusedId, focusedEvent, clipboardActions, getLgTemplates, removeLgTemplates, removeLuIntent } = useContext( NodeRendererContext ); + const dereferenceLg = async (lgText: string): Promise => { + const inputLgRef = LgTemplateRef.parse(lgText); + if (!inputLgRef) return lgText; + + const lgTemplates = await getLgTemplates(inputLgRef.name); + if (!Array.isArray(lgTemplates) || !lgTemplates.length) return lgText; + + const targetTemplate = lgTemplates.find(x => x.name === inputLgRef.name); + return targetTemplate ? targetTemplate.body : lgText; + }; + const deleteLgTemplates = (lgTemplates: string[]) => { const normalizedLgTemplates = lgTemplates .map(x => { @@ -91,25 +102,8 @@ export const ObiEditor: FC = ({ case NodeEventTypes.Insert: if (eventData.$type === 'PASTE') { handler = e => { - // TODO: clean this along with node deletion. - const copyLgTemplateToNewNode = async (lgText: string, newNodeId: string) => { - const inputLgRef = LgTemplateRef.parse(lgText); - if (!inputLgRef) return lgText; - - const inputLgMetaData = LgMetaData.parse(inputLgRef.name); - if (!inputLgMetaData) return lgText; - - inputLgMetaData.designerId = newNodeId; - const newLgName = inputLgMetaData.toString(); - const newLgTemplateRefString = new LgTemplateRef(newLgName).toString(); - - const lgFileId = path; - await copyLgTemplate(lgFileId, inputLgRef.name, newLgName); - return newLgTemplateRefString; - }; - pasteNodes(data, e.id, e.position, clipboardActions, copyLgTemplateToNewNode).then(dialog => { - onChange(dialog); - }); + const dialog = pasteNodes(data, e.id, e.position, clipboardActions); + onChange(dialog); }; } else { handler = e => { @@ -128,16 +122,18 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.CopySelection: handler = e => { - const copiedActions = copyNodes(data, e.actionIds); - onClipboardChange(copiedActions); + copyNodes(data, e.actionIds, dereferenceLg).then(copiedNodes => onClipboardChange(copiedNodes)); }; break; case NodeEventTypes.CutSelection: handler = e => { - const { dialog, cutData } = cutNodes(data, e.actionIds); - onChange(dialog); - onFocusSteps([]); - onClipboardChange(cutData); + cutNodes(data, e.actionIds, dereferenceLg, nodes => + deleteActions(nodes, deleteLgTemplates, deleteLuIntents) + ).then(({ dialog, cutData }) => { + onChange(dialog); + onFocusSteps([]); + onClipboardChange(cutData); + }); }; 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 e6ab0647fa..765b8a4858 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -117,7 +117,7 @@ export function deleteNode(inputDialog, path, callbackOnRemovedData?: (removedDa return dialog; } -export function deleteNodes(inputDialog, nodeIds: string[], callbackOnRemovedData?: (removedData: any) => any) { +export function deleteNodes(inputDialog, nodeIds: string[], callbackOnRemovedNodes?: (nodes: any[]) => any) { const dialog = cloneDeep(inputDialog); const nodeLocations = nodeIds.map(id => locateNode(dialog, id)); @@ -144,8 +144,8 @@ export function deleteNodes(inputDialog, nodeIds: string[], callbackOnRemovedDat }); // invoke callback handler - if (callbackOnRemovedData && typeof callbackOnRemovedData === 'function') { - deletedNodes.forEach(x => callbackOnRemovedData(x)); + if (callbackOnRemovedNodes && typeof callbackOnRemovedNodes === 'function') { + callbackOnRemovedNodes(deletedNodes); } return dialog; @@ -168,14 +168,31 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -export function copyNodes(inputDialog, nodeIds: string[]): any[] { +type DereferenceLgHandler = (lgTemplateName: string) => Promise; + +export async function copyNodes(inputDialog, nodeIds: string[], dereferenceLg: DereferenceLgHandler): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - return JSON.parse(JSON.stringify(nodes)); + + // NOTES: underlying lg api for writing new lg template to file is not concurrency-safe, + // so we have to call them sequentially + // TODO: copy them parralleled via Promise.all() after optimizing lg api. + const copiedNodes: any[] = []; + for (const node of nodes) { + // Deep copy nodes with external resources + const copy = await deepCopyAction(node, dereferenceLg); + copiedNodes.push(copy); + } + return copiedNodes; } -export function cutNodes(inputDialog, nodeIds: string[]) { - const nodesData = copyNodes(inputDialog, nodeIds); - const newDialog = deleteNodes(inputDialog, nodeIds); +export async function cutNodes( + inputDialog, + nodeIds: string[], + dereferenceLg: DereferenceLgHandler, + callbackOnCutNodes?: (nodes: any[]) => any +) { + const nodesData = await copyNodes(inputDialog, nodeIds, dereferenceLg); + const newDialog = deleteNodes(inputDialog, nodeIds, callbackOnCutNodes); return { dialog: newDialog, cutData: nodesData }; } @@ -196,7 +213,7 @@ export function appendNodesAfter(inputDialog, targetId, newNodes) { return dialog; } -export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, copyLgTemplate) { +export function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes) { if (!Array.isArray(newNodes) || newNodes.length === 0) { return inputDialog; } @@ -208,16 +225,6 @@ export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, c return inputDialog; } - // NOTES: underlying lg api for writing new lg template to file is not concurrency-safe, - // so we have to call them sequentially - // TODO: copy them parralleled via Promise.all() after optimizing lg api. - const copiedNodes: any[] = []; - for (const node of newNodes) { - // Deep copy nodes with external resources - const copy = await deepCopyAction(node, copyLgTemplate); - copiedNodes.push(copy); - } - - targetArray.currentData.splice(arrayIndex, 0, ...copiedNodes); + targetArray.currentData.splice(arrayIndex, 0, ...newNodes); return dialog; } From 6f29dc4ed0cb955d1cce3866e64b0e7382faa554 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Wed, 11 Mar 2020 23:02:34 +0800 Subject: [PATCH 2/8] implement lg resources walker --- .../lib/shared/src/deleteUtils/index.ts | 2 + Composer/packages/lib/shared/src/index.ts | 1 + .../lib/shared/src/walkerUtils/index.tsx | 4 ++ .../shared/src/walkerUtils/walkLgResources.ts | 40 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 Composer/packages/lib/shared/src/walkerUtils/index.tsx create mode 100644 Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts diff --git a/Composer/packages/lib/shared/src/deleteUtils/index.ts b/Composer/packages/lib/shared/src/deleteUtils/index.ts index 14d10ddfec..c237ba35fc 100644 --- a/Composer/packages/lib/shared/src/deleteUtils/index.ts +++ b/Composer/packages/lib/shared/src/deleteUtils/index.ts @@ -6,6 +6,7 @@ import { MicrosoftIDialog, SDKTypes } from '../types'; import { walkAdaptiveAction } from './walkAdaptiveAction'; import { walkAdaptiveActionList } from './walkAdaptiveActionList'; +// TODO: (ze) considering refactoring it with the `walkLgResources` util const collectLgTemplates = (action: any, outputTemplates: string[]) => { if (typeof action === 'string') return; if (!action || !action.$type) return; @@ -25,6 +26,7 @@ const collectLgTemplates = (action: any, outputTemplates: string[]) => { } }; +// TODO: (ze) considering refactoring it by implementing a new `walkLuResources` util const collectLuIntents = (action: any, outputTemplates: string[]) => { if (typeof action === 'string') return; if (!action || !action.$type) return; diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index 0c0d75b91e..a4069c751e 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -13,3 +13,4 @@ export * from './appschema'; export * from './types'; export * from './constant'; export * from './lgUtils'; +export * from './walkerUtils'; diff --git a/Composer/packages/lib/shared/src/walkerUtils/index.tsx b/Composer/packages/lib/shared/src/walkerUtils/index.tsx new file mode 100644 index 0000000000..9fdea03957 --- /dev/null +++ b/Composer/packages/lib/shared/src/walkerUtils/index.tsx @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { walkLgResources } from './walkLgResources'; diff --git a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts new file mode 100644 index 0000000000..5196c31bcc --- /dev/null +++ b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import get from 'lodash/get'; + +import { walkAdaptiveAction } from '../deleteUtils/walkAdaptiveAction'; +import { SDKTypes } from '../types'; + +type LgFieldHandler = (nodeId: string, lgType: string, lgString: string) => any; + +const findLgFields = (action: any, handleLgField: LgFieldHandler) => { + if (typeof action === 'string') return; + if (!action || !action.$type) return; + + const actionId = get(action, '$designer.id', ''); + const onFound = (fieldName: string) => { + action[fieldName] && handleLgField(actionId, fieldName, action[fieldName]); + }; + + switch (action.$type) { + case SDKTypes.SendActivity: + onFound('activity'); + break; + case SDKTypes.AttachmentInput: + case SDKTypes.ChoiceInput: + case SDKTypes.ConfirmInput: + case SDKTypes.DateTimeInput: + case SDKTypes.NumberInput: + case SDKTypes.TextInput: + onFound('prompt'); + onFound('unrecognizedPrompt'); + onFound('invalidPrompt'); + onFound('defaultValueResponse'); + break; + } +}; + +export const walkLgResources = (action, handleLgResource: LgFieldHandler) => { + walkAdaptiveAction(action, action => findLgFields(action, handleLgResource)); +}; From ec4d75dc5f6475241cdbda9ce758da6cd30e6cbf Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Wed, 11 Mar 2020 23:36:18 +0800 Subject: [PATCH 3/8] update lg walker api --- .../packages/lib/shared/src/walkerUtils/index.tsx | 2 +- .../lib/shared/src/walkerUtils/walkLgResources.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Composer/packages/lib/shared/src/walkerUtils/index.tsx b/Composer/packages/lib/shared/src/walkerUtils/index.tsx index 9fdea03957..5538f89aed 100644 --- a/Composer/packages/lib/shared/src/walkerUtils/index.tsx +++ b/Composer/packages/lib/shared/src/walkerUtils/index.tsx @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export { walkLgResources } from './walkLgResources'; +export { walkLgResourcesInAction, walkLgResourcesInActionList } from './walkLgResources'; diff --git a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts index 5196c31bcc..92916d5ad4 100644 --- a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts +++ b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts @@ -5,16 +5,16 @@ import get from 'lodash/get'; import { walkAdaptiveAction } from '../deleteUtils/walkAdaptiveAction'; import { SDKTypes } from '../types'; +import { walkAdaptiveActionList } from '../deleteUtils/walkAdaptiveActionList'; -type LgFieldHandler = (nodeId: string, lgType: string, lgString: string) => any; +type LgFieldHandler = (action, lgFieldName: string, lgString: string) => any; const findLgFields = (action: any, handleLgField: LgFieldHandler) => { if (typeof action === 'string') return; if (!action || !action.$type) return; - const actionId = get(action, '$designer.id', ''); const onFound = (fieldName: string) => { - action[fieldName] && handleLgField(actionId, fieldName, action[fieldName]); + action[fieldName] && handleLgField(action, fieldName, action[fieldName]); }; switch (action.$type) { @@ -35,6 +35,10 @@ const findLgFields = (action: any, handleLgField: LgFieldHandler) => { } }; -export const walkLgResources = (action, handleLgResource: LgFieldHandler) => { +export const walkLgResourcesInAction = (action, handleLgResource: LgFieldHandler) => { walkAdaptiveAction(action, action => findLgFields(action, handleLgResource)); }; + +export const walkLgResourcesInActionList = (actioList: any[], handleLgResource: LgFieldHandler) => { + walkAdaptiveActionList(actioList, action => findLgFields(action, handleLgResource)); +}; From 8d3d03e22672aa27e161610bdd9ca7352d83f415 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 10:56:18 +0800 Subject: [PATCH 4/8] split insertNodes from pasteNodes --- .../extensions/visual-designer/src/utils/jsonTracker.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 765b8a4858..8840433796 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -213,7 +213,7 @@ export function appendNodesAfter(inputDialog, targetId, newNodes) { return dialog; } -export function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes) { +function insertNodes(inputDialog, arrayPath: string, arrayIndex: number, newNodes: any[]) { if (!Array.isArray(newNodes) || newNodes.length === 0) { return inputDialog; } @@ -228,3 +228,8 @@ export function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes) { targetArray.currentData.splice(arrayIndex, 0, ...newNodes); return dialog; } + +export function pasteNodes(inputDialog, arrayPath: string, arrayIndex: number, clipboardNodes: any[]) { + const newNodes = [...clipboardNodes]; + return insertNodes(inputDialog, arrayPath, arrayIndex, newNodes); +} From e2131794cbaa908b204fb157f93a0311a2f89b3c Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 11:03:58 +0800 Subject: [PATCH 5/8] fix tslint --- Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts index 92916d5ad4..faf75eb066 100644 --- a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts +++ b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import get from 'lodash/get'; - import { walkAdaptiveAction } from '../deleteUtils/walkAdaptiveAction'; import { SDKTypes } from '../types'; import { walkAdaptiveActionList } from '../deleteUtils/walkAdaptiveActionList'; From 1f76348b2044af354723aa6b2d9a80ff3c720cc9 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 11:53:44 +0800 Subject: [PATCH 6/8] change copyUtils ExtarnelAPI interface --- .../visual-designer/src/utils/jsonTracker.ts | 18 +++++----------- .../copyUtils/copyInputDialog.test.ts | 2 +- .../copyUtils/copySendActivity.test.ts | 2 +- .../__tests__/jestMocks/externalApiStub.ts | 2 +- .../lib/shared/src/copyUtils/ExternalApi.ts | 16 +++++++++++++- .../shared/src/copyUtils/copyInputDialog.ts | 9 ++++---- .../shared/src/copyUtils/copySendActivity.ts | 2 +- .../packages/lib/shared/src/dialogFactory.ts | 21 ++++++++++++++----- 8 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 8840433796..e664d699ed 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -4,7 +4,7 @@ import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; import set from 'lodash/set'; -import { seedNewDialog, deepCopyAction, generateSDKTitle } from '@bfc/shared'; +import { seedNewDialog, deepCopyActions, generateSDKTitle } from '@bfc/shared'; function parseSelector(path: string): null | string[] { if (!path) return null; @@ -172,17 +172,7 @@ type DereferenceLgHandler = (lgTemplateName: string) => Promise; export async function copyNodes(inputDialog, nodeIds: string[], dereferenceLg: DereferenceLgHandler): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); - - // NOTES: underlying lg api for writing new lg template to file is not concurrency-safe, - // so we have to call them sequentially - // TODO: copy them parralleled via Promise.all() after optimizing lg api. - const copiedNodes: any[] = []; - for (const node of nodes) { - // Deep copy nodes with external resources - const copy = await deepCopyAction(node, dereferenceLg); - copiedNodes.push(copy); - } - return copiedNodes; + return deepCopyActions(nodes, dereferenceLg); } export async function cutNodes( @@ -230,6 +220,8 @@ function insertNodes(inputDialog, arrayPath: string, arrayIndex: number, newNode } export function pasteNodes(inputDialog, arrayPath: string, arrayIndex: number, clipboardNodes: any[]) { - const newNodes = [...clipboardNodes]; + // Considering a scenario that copy one time but paste multiple times, + // it requires seeding all $designer.id again by invoking deepCopy. + const newNodes = deepCopyActions(clipboardNodes); return insertNodes(inputDialog, arrayPath, arrayIndex, newNodes); } diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts index 918ef04eaf..1e0dd78bfd 100644 --- a/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts +++ b/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts @@ -8,7 +8,7 @@ import { externalApiStub as externalApi } from '../jestMocks/externalApiStub'; describe('shallowCopyAdaptiveAction', () => { const externalApiWithLgCopy: ExternalApi = { ...externalApi, - copyLgTemplate: (templateName, newNodeId) => Promise.resolve(templateName + '(copy)'), + copyLgTemplate: (id, data, field, value) => Promise.resolve(value + '(copy)'), }; it('can copy TextInput', async () => { diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts index 8a30e66c53..12c7682f89 100644 --- a/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts +++ b/Composer/packages/lib/shared/__tests__/copyUtils/copySendActivity.test.ts @@ -8,7 +8,7 @@ import { externalApiStub as externalApi } from '../jestMocks/externalApiStub'; describe('copySendActivity', () => { const externalApiWithLgCopy: ExternalApi = { ...externalApi, - copyLgTemplate: (templateName, newNodeId) => Promise.resolve(templateName + '(copy)'), + copyLgTemplate: (id, data, fieldName, fieldValue) => Promise.resolve(fieldValue + '(copy)'), }; it('can copy SendActivity', async () => { diff --git a/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts b/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts index c8e297b99d..7aa455f302 100644 --- a/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts +++ b/Composer/packages/lib/shared/__tests__/jestMocks/externalApiStub.ts @@ -5,5 +5,5 @@ import { ExternalApi } from '../../src/copyUtils/ExternalApi'; export const externalApiStub: ExternalApi = { getDesignerId: () => ({ id: '5678' }), - copyLgTemplate: (lgTemplateName: string, targetNodeId: string) => Promise.resolve(lgTemplateName), + copyLgTemplate: (id, data, fieldName, fieldValue) => Promise.resolve(fieldValue || ''), }; diff --git a/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts b/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts index 2f86b9aa01..fe586e5924 100644 --- a/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts +++ b/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts @@ -3,7 +3,21 @@ import { DesignerData } from '../types'; +export type ExternalResourceCopyHandler = ( + actionId: string, + actionData: any, + resourceFieldName: string, + resourceValue?: CopiedType +) => CopiedType; + +export type ExternalResourceCopyHandlerAsync = ( + actionId: string, + actionData: any, + resourceFieldName: string, + resourceValue?: CopiedType +) => Promise; + export interface ExternalApi { getDesignerId: (data?: DesignerData) => DesignerData; - copyLgTemplate: (lgTemplateName: string, newNodeId: string) => Promise; + copyLgTemplate: ExternalResourceCopyHandlerAsync; } diff --git a/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts index 5371b69e04..71eda21774 100644 --- a/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts +++ b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts @@ -9,21 +9,22 @@ 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 : ''; + const copyLgField = (data, fieldName: string) => externalApi.copyLgTemplate(nodeId, data, fieldName, data[fieldName]); if (input.prompt !== undefined) { - copy.prompt = await externalApi.copyLgTemplate(input.prompt, nodeId); + copy.prompt = await copyLgField(copy, 'prompt'); } if (input.unrecognizedPrompt !== undefined) { - copy.unrecognizedPrompt = await externalApi.copyLgTemplate(input.unrecognizedPrompt, nodeId); + copy.unrecognizedPrompt = await copyLgField(copy, 'unrecognizedPrompt'); } if (input.invalidPrompt !== undefined) { - copy.invalidPrompt = await externalApi.copyLgTemplate(input.invalidPrompt, nodeId); + copy.invalidPrompt = await copyLgField(copy, 'invalidPrompt'); } if (input.defaultValueResponse !== undefined) { - copy.defaultValueResponse = await externalApi.copyLgTemplate(input.defaultValueResponse, nodeId); + copy.defaultValueResponse = await copyLgField(copy, 'defaultValueResponse'); } return copy; diff --git a/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts b/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts index 54c22113c4..929201c322 100644 --- a/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts +++ b/Composer/packages/lib/shared/src/copyUtils/copySendActivity.ts @@ -11,7 +11,7 @@ export const copySendActivity = async (input: SendActivity, externalApi: Externa const nodeId = copy.$designer ? copy.$designer.id : ''; if (input.activity !== undefined) { - copy.activity = await externalApi.copyLgTemplate(input.activity, nodeId); + copy.activity = await externalApi.copyLgTemplate(nodeId, copy, 'activity', copy.activity); } return copy; diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 585b5a6aed..7ed461a0f0 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -9,6 +9,7 @@ import { copyAdaptiveAction } from './copyUtils'; import { deleteAdaptiveAction, deleteAdaptiveActionList } from './deleteUtils'; import { MicrosoftIDialog } from './types'; import { SDKTypes } from './types'; +import { ExternalResourceCopyHandlerAsync } from './copyUtils/ExternalApi'; interface DesignerAttributes { name: string; description: string; @@ -108,16 +109,26 @@ export const seedDefaults = (type: string) => { return assignDefaults(properties); }; -export const deepCopyAction = async ( - data, - copyLgTemplateToNewNode: (lgTemplateName: string, newNodeId: string) => Promise -) => { +export const deepCopyAction = async (data, copyLgTemplate: ExternalResourceCopyHandlerAsync) => { return await copyAdaptiveAction(data, { getDesignerId, - copyLgTemplate: copyLgTemplateToNewNode, + copyLgTemplate, }); }; +export const deepCopyActions = async (actions: any[], copyLgTemplate: ExternalResourceCopyHandlerAsync) => { + // NOTES: underlying lg api for writing new lg template to file is not concurrency-safe, + // so we have to call them sequentially + // TODO: copy them parralleled via Promise.all() after optimizing lg api. + const copiedActions: any[] = []; + for (const action of actions) { + // Deep copy nodes with external resources + const copy = await deepCopyAction(action, copyLgTemplate); + copiedActions.push(copy); + } + return copiedActions; +}; + export const deleteAction = ( data: MicrosoftIDialog, deleteLgTemplates: (templates: string[]) => any, From 4e653246d328f53f0c8fbe50003db8cbbe2c6cf9 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 12:44:00 +0800 Subject: [PATCH 7/8] migrate to new api format --- .../visual-designer/src/editors/ObiEditor.tsx | 13 ++++++++++--- .../visual-designer/src/utils/jsonTracker.ts | 14 ++++++++++---- .../packages/lib/shared/src/copyUtils/index.ts | 1 + Composer/packages/lib/shared/src/index.ts | 1 + 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 6357a1d73b..4ef5041c31 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,7 +5,7 @@ import { jsx } from '@emotion/core'; import { useContext, FC, useEffect, useState, useRef } from 'react'; import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { deleteAction, deleteActions, LgTemplateRef } from '@bfc/shared'; +import { deleteAction, deleteActions, LgTemplateRef, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -48,7 +48,14 @@ export const ObiEditor: FC = ({ NodeRendererContext ); - const dereferenceLg = async (lgText: string): Promise => { + const dereferenceLg: ExternalResourceCopyHandlerAsync = async ( + actionId: string, + actionData: any, + lgFieldName: string, + lgText?: string + ): Promise => { + if (!lgText) return ''; + const inputLgRef = LgTemplateRef.parse(lgText); if (!inputLgRef) return lgText; @@ -102,7 +109,7 @@ export const ObiEditor: FC = ({ case NodeEventTypes.Insert: if (eventData.$type === 'PASTE') { handler = e => { - const dialog = pasteNodes(data, e.id, e.position, clipboardActions); + const dialog = pasteNodes(data, e.id, e.position, clipboardActions, dereferenceLg); onChange(dialog); }; } else { diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index e664d699ed..96e010e425 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -4,7 +4,7 @@ import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; import set from 'lodash/set'; -import { seedNewDialog, deepCopyActions, generateSDKTitle } from '@bfc/shared'; +import { seedNewDialog, deepCopyActions, generateSDKTitle, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; function parseSelector(path: string): null | string[] { if (!path) return null; @@ -168,7 +168,7 @@ export function insert(inputDialog, path, position, $type) { return dialog; } -type DereferenceLgHandler = (lgTemplateName: string) => Promise; +type DereferenceLgHandler = ExternalResourceCopyHandlerAsync; export async function copyNodes(inputDialog, nodeIds: string[], dereferenceLg: DereferenceLgHandler): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); @@ -219,9 +219,15 @@ function insertNodes(inputDialog, arrayPath: string, arrayIndex: number, newNode return dialog; } -export function pasteNodes(inputDialog, arrayPath: string, arrayIndex: number, clipboardNodes: any[]) { +export async function pasteNodes( + inputDialog, + arrayPath: string, + arrayIndex: number, + clipboardNodes: any[], + handleLgField: ExternalResourceCopyHandlerAsync +) { // Considering a scenario that copy one time but paste multiple times, // it requires seeding all $designer.id again by invoking deepCopy. - const newNodes = deepCopyActions(clipboardNodes); + const newNodes = await deepCopyActions(clipboardNodes, handleLgField); return insertNodes(inputDialog, arrayPath, arrayIndex, newNodes); } diff --git a/Composer/packages/lib/shared/src/copyUtils/index.ts b/Composer/packages/lib/shared/src/copyUtils/index.ts index c04dd59fa5..d6bd0088a7 100644 --- a/Composer/packages/lib/shared/src/copyUtils/index.ts +++ b/Composer/packages/lib/shared/src/copyUtils/index.ts @@ -2,3 +2,4 @@ // Licensed under the MIT License. export { copyAdaptiveAction } from './copyAdaptiveAction'; +export { ExternalResourceCopyHandlerAsync } from './ExternalApi'; diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index a4069c751e..df4c90189c 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -14,3 +14,4 @@ export * from './types'; export * from './constant'; export * from './lgUtils'; export * from './walkerUtils'; +export * from './copyUtils'; From 8f24ed1e3626f238b8b918e3a66e545f9a0612c3 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 14:18:46 +0800 Subject: [PATCH 8/8] create real lg template when pasting --- .../visual-designer/src/editors/ObiEditor.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 4ef5041c31..7004490b90 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,7 +5,7 @@ import { jsx } from '@emotion/core'; import { useContext, FC, useEffect, useState, useRef } from 'react'; import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { deleteAction, deleteActions, LgTemplateRef, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; +import { deleteAction, deleteActions, LgTemplateRef, LgMetaData, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -44,9 +44,15 @@ export const ObiEditor: FC = ({ }): JSX.Element | null => { let divRef; - const { focusedId, focusedEvent, clipboardActions, getLgTemplates, removeLgTemplates, removeLuIntent } = useContext( - NodeRendererContext - ); + const { + focusedId, + focusedEvent, + clipboardActions, + getLgTemplates, + updateLgTemplate, + removeLgTemplates, + removeLuIntent, + } = useContext(NodeRendererContext); const dereferenceLg: ExternalResourceCopyHandlerAsync = async ( actionId: string, @@ -66,6 +72,14 @@ export const ObiEditor: FC = ({ return targetTemplate ? targetTemplate.body : lgText; }; + const buildLgReference: ExternalResourceCopyHandlerAsync = async (nodeId, data, fieldName, fieldText) => { + if (!fieldText) return ''; + const newLgTemplateName = new LgMetaData(fieldName, nodeId).toString(); + const newLgTemplateRefStr = new LgTemplateRef(newLgTemplateName).toString(); + await updateLgTemplate(path, newLgTemplateName, fieldText); + return newLgTemplateRefStr; + }; + const deleteLgTemplates = (lgTemplates: string[]) => { const normalizedLgTemplates = lgTemplates .map(x => { @@ -109,8 +123,9 @@ export const ObiEditor: FC = ({ case NodeEventTypes.Insert: if (eventData.$type === 'PASTE') { handler = e => { - const dialog = pasteNodes(data, e.id, e.position, clipboardActions, dereferenceLg); - onChange(dialog); + pasteNodes(data, e.id, e.position, clipboardActions, buildLgReference).then(dialog => { + onChange(dialog); + }); }; } else { handler = e => {