From 42164f7eed8e43038b98ccdeed062cd4f4f5a8d2 Mon Sep 17 00:00:00 2001 From: Chris Whitten Date: Sat, 7 Mar 2020 19:17:34 -0800 Subject: [PATCH 1/6] Implement Move --- Composer/packages/client/src/ShellApi.ts | 4 +-- .../ExtensionContainer.tsx | 4 +-- .../packages/client/src/messenger/FrameAPI.ts | 1 + .../client/src/pages/design/index.tsx | 14 ++++++++- .../client/src/store/action/dialog.ts | 3 +- Composer/packages/client/src/store/index.tsx | 1 + .../client/src/store/reducer/index.ts | 4 ++- Composer/packages/client/src/store/types.ts | 1 + .../src/Form/widgets/DialogSelectWidget.tsx | 2 +- .../src/constants/NodeEventTypes.ts | 1 + .../visual-designer/src/editors/ObiEditor.tsx | 30 +++++++++++++++++++ .../extensions/visual-designer/src/index.tsx | 2 ++ .../visual-designer/src/utils/jsonTracker.ts | 4 +-- .../packages/lib/shared/src/dialogFactory.ts | 30 ++++++++++--------- .../packages/lib/shared/src/types/shell.ts | 2 +- 15 files changed, 78 insertions(+), 25 deletions(-) diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index ed3dd20dea..07efa47db6 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -351,9 +351,9 @@ export const ShellApi: React.FC = () => { apiClient.registerApi('onSelect', onSelect); apiClient.registerApi('onCopy', onCopy); apiClient.registerApi('isExpression', ({ expression }) => isExpression(expression)); - apiClient.registerApi('createDialog', () => { + apiClient.registerApi('createDialog', actionsSeed => { return new Promise(resolve => { - actions.createDialogBegin((newDialog: string | null) => { + actions.createDialogBegin(actionsSeed, (newDialog: string | null) => { resolve(newDialog); }); }); diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index 9ad680cfd9..5f0f9f31ce 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -110,8 +110,8 @@ const shellApi: ShellApi = { return apiClient.apiCall('removeLuIntent', { id, intentName }); }, - createDialog: () => { - return apiClient.apiCall('createDialog'); + createDialog: actions => { + return apiClient.apiCall('createDialog', { actions }); }, validateExpression: expression => { diff --git a/Composer/packages/client/src/messenger/FrameAPI.ts b/Composer/packages/client/src/messenger/FrameAPI.ts index e5ef121b07..382c92369b 100644 --- a/Composer/packages/client/src/messenger/FrameAPI.ts +++ b/Composer/packages/client/src/messenger/FrameAPI.ts @@ -42,6 +42,7 @@ export const VisualEditorAPI = (() => { hasElementSelected: () => visualEditorFrameAPI.invoke('hasElementSelected').catch(() => false), copySelection: () => visualEditorFrameAPI.invoke('copySelection'), cutSelection: () => visualEditorFrameAPI.invoke('cutSelection'), + moveSelection: () => visualEditorFrameAPI.invoke('moveSelection'), deleteSelection: () => visualEditorFrameAPI.invoke('deleteSelection'), }; })(); diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 25655f874f..1855fb7fd9 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -251,6 +251,18 @@ function DesignPage(props) { align: 'left', disabled: !nodeOperationAvailable, }, + { + type: 'action', + text: formatMessage('Move'), + buttonProps: { + iconProps: { + iconName: 'Share', + }, + onClick: () => VisualEditorAPI.moveSelection(), + }, + align: 'left', + disabled: !nodeOperationAvailable, + }, { type: 'action', text: formatMessage('Delete'), @@ -309,7 +321,7 @@ function DesignPage(props) { async function onSubmit(data: { name: string; description: string }) { const content = { ...getNewDesigner(data.name, data.description), generator: `${data.name}.lg` }; - const seededContent = seedNewDialog('Microsoft.AdaptiveDialog', content.$designer, content); + const seededContent = seedNewDialog('Microsoft.AdaptiveDialog', content.$designer, content, state.actionsSeed); await actions.createDialog({ id: data.name, content: seededContent }); } diff --git a/Composer/packages/client/src/store/action/dialog.ts b/Composer/packages/client/src/store/action/dialog.ts index bcd86e3c52..248fadae62 100644 --- a/Composer/packages/client/src/store/action/dialog.ts +++ b/Composer/packages/client/src/store/action/dialog.ts @@ -132,10 +132,11 @@ export const updateDialog: ActionCreator = undoable( updateDialogBase ); -export const createDialogBegin: ActionCreator = ({ dispatch }, onComplete) => { +export const createDialogBegin: ActionCreator = ({ dispatch }, { actions }, onComplete) => { dispatch({ type: ActionTypes.CREATE_DIALOG_BEGIN, payload: { + actionsSeed: actions, onComplete, }, }); diff --git a/Composer/packages/client/src/store/index.tsx b/Composer/packages/client/src/store/index.tsx index 7c7817848c..7278654e63 100644 --- a/Composer/packages/client/src/store/index.tsx +++ b/Composer/packages/client/src/store/index.tsx @@ -36,6 +36,7 @@ const initialState: State = { lgFiles: [], schemas: { editor: {} }, luFiles: [], + actionsSeed: [], designPageLocation: { dialogId: '', focused: '', diff --git a/Composer/packages/client/src/store/reducer/index.ts b/Composer/packages/client/src/store/reducer/index.ts index 79cb94e55a..398409bb48 100644 --- a/Composer/packages/client/src/store/reducer/index.ts +++ b/Composer/packages/client/src/store/reducer/index.ts @@ -88,8 +88,9 @@ const removeDialog: ReducerFunc = (state, { response }) => { return state; }; -const createDialogBegin: ReducerFunc = (state, { onComplete }) => { +const createDialogBegin: ReducerFunc = (state, { actionsSeed, onComplete }) => { state.showCreateDialogModal = true; + state.actionsSeed = actionsSeed; state.onCreateDialogComplete = onComplete; return state; }; @@ -105,6 +106,7 @@ const createDialogSuccess: ReducerFunc = (state, { response }) => { state.luFiles = response.data.luFiles; state.lgFiles = response.data.lgFiles; state.showCreateDialogModal = false; + state.actionsSeed = []; delete state.onCreateDialogComplete; return state; }; diff --git a/Composer/packages/client/src/store/types.ts b/Composer/packages/client/src/store/types.ts index 50f1cfc33b..3ed8dab254 100644 --- a/Composer/packages/client/src/store/types.ts +++ b/Composer/packages/client/src/store/types.ts @@ -72,6 +72,7 @@ export interface State { showCreateDialogModal: boolean; isEnvSettingUpdated: boolean; settings: DialogSetting; + actionsSeed: any; onCreateDialogComplete?: (dialogId: string | null) => void; toStartBot: boolean; currentUser: { diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/DialogSelectWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/DialogSelectWidget.tsx index a625c6148d..95a4ef22a7 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/DialogSelectWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/DialogSelectWidget.tsx @@ -48,7 +48,7 @@ export const DialogSelectWidget: React.FC = props => { if (option) { if (option.key === ADD_DIALOG) { setComboboxTitle(formatMessage('Create a new dialog')); - formContext.shellApi.createDialog().then(newDialog => { + formContext.shellApi.createDialog({ actions: [] }).then(newDialog => { if (newDialog) { onChange(newDialog); setTimeout(() => formContext.shellApi.navTo(newDialog), 500); diff --git a/Composer/packages/extensions/visual-designer/src/constants/NodeEventTypes.ts b/Composer/packages/extensions/visual-designer/src/constants/NodeEventTypes.ts index 23a204d101..83a8786273 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/NodeEventTypes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/NodeEventTypes.ts @@ -13,6 +13,7 @@ export enum NodeEventTypes { InsertEvent = 'event.data.insert-event', CopySelection = 'event.data.copy-selection', CutSelection = 'event.data.cut-selection', + MoveSelection = 'event.data.move-selection', DeleteSelection = 'event.data.delete-selection', AppendSelection = 'event.data.paste-selection--keyboard', InsertSelection = 'event.data.paste-selection--menu', diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 8c1c35a255..d144882a94 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -6,6 +6,8 @@ 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 querystring from 'query-string'; +import { SDKTypes } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -37,6 +39,7 @@ export const ObiEditor: FC = ({ onClipboardChange, onOpen, onChange, + onCreateDialog, onSelect, undo, redo, @@ -140,6 +143,30 @@ export const ObiEditor: FC = ({ onClipboardChange(cutData); }; break; + case NodeEventTypes.MoveSelection: + handler = e => { + const copiedActions = copyNodes(data, e.actionIds); + onCreateDialog(copiedActions).then(d => { + const startIndex = e.actionIds[0].indexOf('actions['); + const position = parseInt(e.actionIds[0][startIndex + 8]); + const deleteResult = deleteNodes(data, e.actionIds, nodes => + deleteActions(nodes, deleteLgTemplates, deleteLuIntents) + ); + const queryString = querystring.parseUrl(parent.location.href); + const insertResult = insert( + deleteResult, + `${queryString.query.selected}.actions`, + position, + SDKTypes.BeginDialog, + { + dialog: d, + } + ); + onChange(insertResult); + }); + onFocusSteps([]); + }; + break; case NodeEventTypes.DeleteSelection: handler = e => { const dialog = deleteNodes(data, e.actionIds, nodes => @@ -239,6 +266,8 @@ export const ObiEditor: FC = ({ dispatchEvent(NodeEventTypes.CopySelection, { actionIds: getClipboardTargetsFromContext() }); (window as any).cutSelection = () => dispatchEvent(NodeEventTypes.CutSelection, { actionIds: getClipboardTargetsFromContext() }); + (window as any).moveSelection = () => + dispatchEvent(NodeEventTypes.MoveSelection, { actionIds: getClipboardTargetsFromContext() }); (window as any).deleteSelection = () => dispatchEvent(NodeEventTypes.DeleteSelection, { actionIds: getClipboardTargetsFromContext() }); @@ -352,6 +381,7 @@ interface ObiEditorProps { focusedEvent: string; onFocusEvent: (eventId: string) => any; onClipboardChange: (actions: any[]) => void; + onCreateDialog: (actions: any[]) => Promise; onOpen: (calleeDialog: string, callerId: string) => any; onChange: (newDialog: any) => any; onSelect: (ids: string[]) => any; diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx index 974b59904f..625747d1d0 100644 --- a/Composer/packages/extensions/visual-designer/src/index.tsx +++ b/Composer/packages/extensions/visual-designer/src/index.tsx @@ -57,6 +57,7 @@ const VisualDesigner: React.FC = ({ onSelect, onCopy, saveData, + createDialog, updateLgTemplate, getLgTemplates, copyLgTemplate, @@ -97,6 +98,7 @@ const VisualDesigner: React.FC = ({ focusedEvent={focusedEvent} onFocusEvent={onFocusEvent} onClipboardChange={onCopy} + onCreateDialog={createDialog} onOpen={x => navTo(x)} onChange={x => saveData(x)} onSelect={onSelect} diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index e6ab0647fa..ac84e6e101 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -151,12 +151,12 @@ export function deleteNodes(inputDialog, nodeIds: string[], callbackOnRemovedDat return dialog; } -export function insert(inputDialog, path, position, $type) { +export function insert(inputDialog, path, position, $type, extra = {}) { const dialog = cloneDeep(inputDialog); const current = get(dialog, path, []); const newStep = { $type, - ...seedNewDialog($type, { name: generateSDKTitle({ $type }) }), + ...seedNewDialog($type, { name: generateSDKTitle({ $type }) }, extra), }; const insertAt = typeof position === 'undefined' ? current.length : position; diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 585b5a6aed..3e2d82c73a 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -23,16 +23,17 @@ const initialInputDialog = { }; const initialDialogShape = { - [SDKTypes.AdaptiveDialog]: { + [SDKTypes.AdaptiveDialog]: seededActions => ({ $type: SDKTypes.AdaptiveDialog, triggers: [ { $type: SDKTypes.OnBeginDialog, ...getNewDesigner('BeginDialog', ''), + actions: [...seededActions], }, ], - }, - [SDKTypes.OnConversationUpdateActivity]: { + }), + [SDKTypes.OnConversationUpdateActivity]: () => ({ $type: 'Microsoft.OnConversationUpdateActivity', actions: [ { @@ -55,16 +56,16 @@ const initialDialogShape = { ], }, ], - }, - [SDKTypes.SendActivity]: { + }), + [SDKTypes.SendActivity]: () => ({ activity: '', - }, - [SDKTypes.AttachmentInput]: initialInputDialog, - [SDKTypes.ChoiceInput]: initialInputDialog, - [SDKTypes.ConfirmInput]: initialInputDialog, - [SDKTypes.DateTimeInput]: initialInputDialog, - [SDKTypes.NumberInput]: initialInputDialog, - [SDKTypes.TextInput]: initialInputDialog, + }), + [SDKTypes.AttachmentInput]: () => initialInputDialog, + [SDKTypes.ChoiceInput]: () => initialInputDialog, + [SDKTypes.ConfirmInput]: () => initialInputDialog, + [SDKTypes.DateTimeInput]: () => initialInputDialog, + [SDKTypes.NumberInput]: () => initialInputDialog, + [SDKTypes.TextInput]: () => initialInputDialog, }; export function getNewDesigner(name: string, description: string) { @@ -137,7 +138,8 @@ export const deleteActions = ( export const seedNewDialog = ( $type: string, designerAttributes: Partial = {}, - optionalAttributes: object = {} + optionalAttributes: object = {}, + seededActions: unknown[] = [] ): object => { return { $type, @@ -146,7 +148,7 @@ export const seedNewDialog = ( ...designerAttributes, }, ...seedDefaults($type), - ...(initialDialogShape[$type] || {}), + ...(initialDialogShape[$type] ? initialDialogShape[$type](seededActions) : {}), ...optionalAttributes, }; }; diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 521d3600a1..e1a4a1d177 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -73,7 +73,7 @@ export interface ShellApi { addLuIntent: (id: string, intent: LuIntentSection | null) => Promise; updateLuIntent: (id: string, intentName: string, intent: LuIntentSection | null) => Promise; removeLuIntent: (id: string, intentName: string) => Promise; - createDialog: () => Promise; + createDialog: (actions: any) => Promise; validateExpression: (expression?: string) => Promise; // TODO: fix these types addCoachMarkRef: any; From 54036aa3eb0a4f0411f6d55838427394362bffb1 Mon Sep 17 00:00:00 2001 From: Chris Whitten Date: Tue, 10 Mar 2020 08:50:43 -0700 Subject: [PATCH 2/6] Rmemove dependency on query string --- .../visual-designer/src/editors/ObiEditor.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index d144882a94..b990d4facc 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -6,7 +6,6 @@ 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 querystring from 'query-string'; import { SDKTypes } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; @@ -146,22 +145,18 @@ export const ObiEditor: FC = ({ case NodeEventTypes.MoveSelection: handler = e => { const copiedActions = copyNodes(data, e.actionIds); - onCreateDialog(copiedActions).then(d => { - const startIndex = e.actionIds[0].indexOf('actions['); + onCreateDialog(copiedActions).then(newDialog => { + // get insert position of BeginDialog + const startIndex = parseInt(e.actionIds[0].replace(/.*\w\[\d+\]\.\w+\[(\d+)\]/, '$1')); + console.log('rrr', e.actionIds[0].replace(/.*\w\[\d+\]\.\w+\[(\d+)\]/, '$1')); const position = parseInt(e.actionIds[0][startIndex + 8]); + console.log('pp', position); const deleteResult = deleteNodes(data, e.actionIds, nodes => deleteActions(nodes, deleteLgTemplates, deleteLuIntents) ); - const queryString = querystring.parseUrl(parent.location.href); - const insertResult = insert( - deleteResult, - `${queryString.query.selected}.actions`, - position, - SDKTypes.BeginDialog, - { - dialog: d, - } - ); + const insertResult = insert(deleteResult, `${focusedEvent}.actions`, position, SDKTypes.BeginDialog, { + dialog: newDialog, + }); onChange(insertResult); }); onFocusSteps([]); From e13cdbb4ce5a44d3e2c094907502d87847ae5b6f Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Wed, 11 Mar 2020 09:55:37 +0800 Subject: [PATCH 3/6] fix: sort action ids correctly (#2217) * sort actionIds by tree path order * filter invalid ids and update test cases --- .../utils/normalizeSelection.test.ts | 52 +++++++++++++++++++ .../src/utils/normalizeSelection.ts | 43 ++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 Composer/packages/extensions/visual-designer/__tests__/utils/normalizeSelection.test.ts diff --git a/Composer/packages/extensions/visual-designer/__tests__/utils/normalizeSelection.test.ts b/Composer/packages/extensions/visual-designer/__tests__/utils/normalizeSelection.test.ts new file mode 100644 index 0000000000..343a08af76 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/__tests__/utils/normalizeSelection.test.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { normalizeSelection, sortActionIds } from '../../src/utils/normalizeSelection'; + +describe('normalizeSelection', () => { + it('should filter out child ids', () => { + const selectedIds1 = ['actions[0]', 'actions[0].actions[0]', 'actions[0].actions[1]']; + expect(normalizeSelection(selectedIds1)).toEqual(['actions[0]']); + + const selectedIds2 = ['actions[0]', 'actions[0].actions[0]', 'actions[0].actions[1]', 'actions[1]', 'actions[1].a']; + expect(normalizeSelection(selectedIds2)).toEqual(['actions[0]', 'actions[1]']); + }); + + it('should keep orphan child ids', () => { + const selectedIds = ['actions[0]', 'actions[0].actions[0]', 'actions[1].actions[0]']; + expect(normalizeSelection(selectedIds)).toEqual(['actions[0]', 'actions[1].actions[0]']); + }); + + it('should throw invalid ids', () => { + const selectedIds = ['action[0].a', 'actions[0].diamond', 'actions', 'actions[0].ifelse']; + expect(normalizeSelection(selectedIds)).toEqual([]); + }); +}); + +describe('sortActionIds', () => { + it('can sort input ids at same level', () => { + const actionIds = ['actions[10]', 'actions[1]', 'actions[3]', 'actions[2]']; + expect(sortActionIds(actionIds)).toEqual(['actions[1]', 'actions[2]', 'actions[3]', 'actions[10]']); + }); + + it('can sort input ids with children', () => { + const actionIds = ['actions[3]', 'actions[2]', 'actions[1].actions[0]', 'actions[1].elseActions[0]', 'actions[1]']; + expect(sortActionIds(actionIds)).toEqual([ + 'actions[1]', + 'actions[1].actions[0]', + 'actions[1].elseActions[0]', + 'actions[2]', + 'actions[3]', + ]); + }); + + it('can sort ids with orphan children', () => { + const actionIds = ['actions[3]', 'actions[2]', 'actions[1].actions[0]', 'actions[1].elseActions[0]']; + expect(sortActionIds(actionIds)).toEqual([ + 'actions[1].actions[0]', + 'actions[1].elseActions[0]', + 'actions[2]', + 'actions[3]', + ]); + }); +}); diff --git a/Composer/packages/extensions/visual-designer/src/utils/normalizeSelection.ts b/Composer/packages/extensions/visual-designer/src/utils/normalizeSelection.ts index aa0ca78fe6..e981fd9104 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/normalizeSelection.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/normalizeSelection.ts @@ -3,11 +3,15 @@ export const normalizeSelection = (selectedIds: string[]): string[] => { if (!Array.isArray(selectedIds)) return []; + + // filter invalid ids such as 'actions[0].diamond' + const validIds = selectedIds.filter(id => id.match(/.*\w+\[\d+\]$/)); // events[0] < events[0].actions[0] < events[1] < events[1].actions[0] - const ascendingIds = [...selectedIds].sort(); + const ascendingIds = sortActionIds(validIds); for (let i = 0; i < ascendingIds.length; i++) { const parentId = ascendingIds[i]; + if (!parentId) continue; for (let j = i + 1; j < ascendingIds.length; j++) { if (ascendingIds[j].startsWith(parentId)) { ascendingIds[j] = ''; @@ -17,3 +21,40 @@ export const normalizeSelection = (selectedIds: string[]): string[] => { return ascendingIds.filter(id => id); }; + +export const sortActionIds = (actionIds: string[]): string[] => { + const parsedActionIds = actionIds.map(id => ({ + id, + paths: id + .split('.') + .map(x => x.replace(/\w+\[(\d+)\]/, '$1')) + .map(x => parseInt(x) || 0), + })); + const sorted = parsedActionIds.sort((a, b) => { + const aPaths = a.paths; + const bPaths = b.paths; + + let diffIndex = 0; + while (diffIndex < aPaths.length && diffIndex < bPaths.length && aPaths[diffIndex] === bPaths[diffIndex]) { + diffIndex++; + } + + const flag = (aPaths[diffIndex] === undefined ? '0' : '1') + (bPaths[diffIndex] === undefined ? '0' : '1'); + switch (flag) { + case '00': + // a equal b ('actions[0]', 'actions[0]') + return 0; + case '01': + // a is b's parent, a < b ('actions[0]', 'actions[0].actions[0]') + return -1; + case '10': + // a is b's child, a > b ('actions[0].actions[0]', 'actions[0]') + return 1; + case '11': + return aPaths[diffIndex] - bPaths[diffIndex]; + default: + return 0; + } + }); + return sorted.map(x => x.id); +}; From 94205449686dae5e1d96184444e5355e258ad0b6 Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Wed, 11 Mar 2020 22:57:17 +0800 Subject: [PATCH 4/6] fix: make MoveSelection work (not target master) (#2234) * make MoveSelection work * add comments --- .../client/src/pages/design/index.tsx | 13 ++++++-- .../visual-designer/src/editors/ObiEditor.tsx | 28 +++++++++++------ .../visual-designer/src/utils/jsonTracker.ts | 16 ++++++---- .../packages/lib/shared/src/dialogFactory.ts | 30 +++++++++---------- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 9cdd99c1a2..7d13e55765 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -11,8 +11,9 @@ import formatMessage from 'format-message'; import { globalHistory } from '@reach/router'; import get from 'lodash/get'; import { PromptTab } from '@bfc/shared'; -import { getNewDesigner, seedNewDialog } from '@bfc/shared'; +import { seedNewDialog } from '@bfc/shared'; import { DialogInfo } from '@bfc/indexers'; +import set from 'lodash/set'; import { VisualEditorAPI } from '../../messenger/FrameAPI'; import { TestController } from '../../TestController'; @@ -324,8 +325,14 @@ function DesignPage(props) { }, [dialogs, breadcrumb]); async function onSubmit(data: { name: string; description: string }) { - const content = { ...getNewDesigner(data.name, data.description), generator: `${data.name}.lg` }; - const seededContent = seedNewDialog('Microsoft.AdaptiveDialog', content.$designer, content, state.actionsSeed); + const seededContent = seedNewDialog( + 'Microsoft.AdaptiveDialog', + { name: data.name, description: data.description }, + { + generator: `${data.name}.lg`, + } + ); + set(seededContent, 'triggers[0].actions', state.actionsSeed || []); await actions.createDialog({ id: data.name, content: seededContent }); } diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index b990d4facc..2e0f237820 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, LgMetaData, seedNewDialog } from '@bfc/shared'; import { SDKTypes } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; @@ -21,6 +21,7 @@ import { appendNodesAfter, pasteNodes, deleteNodes, + insertAction, } from '../utils/jsonTracker'; import { moveCursor, querySelectableElements, SelectorElement } from '../utils/cursorTracker'; import { NodeIndexGenerator } from '../utils/NodeIndexGetter'; @@ -144,19 +145,28 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.MoveSelection: handler = e => { + if (!Array.isArray(e.actionIds) || !e.actionIds.length) return; + + // Using copy-paste-delete pattern here is safer than using cut-paste + // since create new dialog may be cancelled or failed const copiedActions = copyNodes(data, e.actionIds); onCreateDialog(copiedActions).then(newDialog => { - // get insert position of BeginDialog - const startIndex = parseInt(e.actionIds[0].replace(/.*\w\[\d+\]\.\w+\[(\d+)\]/, '$1')); - console.log('rrr', e.actionIds[0].replace(/.*\w\[\d+\]\.\w+\[(\d+)\]/, '$1')); - const position = parseInt(e.actionIds[0][startIndex + 8]); - console.log('pp', position); + // defense modal cancellation + if (!newDialog) return; + + // delete old actions (they are already moved to new dialog) const deleteResult = deleteNodes(data, e.actionIds, nodes => deleteActions(nodes, deleteLgTemplates, deleteLuIntents) ); - const insertResult = insert(deleteResult, `${focusedEvent}.actions`, position, SDKTypes.BeginDialog, { - dialog: newDialog, - }); + + // insert a BeginDialog action points to newly created dialog + const indexes = e.actionIds[0].match(/^(.+)\[(\d+)\]$/); + if (indexes === null || indexes.length !== 3) return; + + const [, arrayPath, actionIndexStr] = indexes; + const startIndex = parseInt(actionIndexStr); + const placeholderAction = seedNewDialog(SDKTypes.BeginDialog, undefined, { dialog: newDialog }); + const insertResult = insertAction(deleteResult, arrayPath, startIndex, placeholderAction); onChange(insertResult); }); onFocusSteps([]); diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index ac84e6e101..f139c5f22b 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts @@ -151,19 +151,23 @@ export function deleteNodes(inputDialog, nodeIds: string[], callbackOnRemovedDat return dialog; } -export function insert(inputDialog, path, position, $type, extra = {}) { - const dialog = cloneDeep(inputDialog); - const current = get(dialog, path, []); +export function insert(inputDialog, path, position, $type) { const newStep = { $type, - ...seedNewDialog($type, { name: generateSDKTitle({ $type }) }, extra), + ...seedNewDialog($type, { name: generateSDKTitle({ $type }) }), }; + return insertAction(inputDialog, path, position, newStep); +} + +export function insertAction(inputDialog, arrayPath: string, position: number, newAction) { + const dialog = cloneDeep(inputDialog); + const current = get(dialog, arrayPath, []); const insertAt = typeof position === 'undefined' ? current.length : position; - current.splice(insertAt, 0, newStep); + current.splice(insertAt, 0, newAction); - set(dialog, path, current); + set(dialog, arrayPath, current); return dialog; } diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 3e2d82c73a..585b5a6aed 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -23,17 +23,16 @@ const initialInputDialog = { }; const initialDialogShape = { - [SDKTypes.AdaptiveDialog]: seededActions => ({ + [SDKTypes.AdaptiveDialog]: { $type: SDKTypes.AdaptiveDialog, triggers: [ { $type: SDKTypes.OnBeginDialog, ...getNewDesigner('BeginDialog', ''), - actions: [...seededActions], }, ], - }), - [SDKTypes.OnConversationUpdateActivity]: () => ({ + }, + [SDKTypes.OnConversationUpdateActivity]: { $type: 'Microsoft.OnConversationUpdateActivity', actions: [ { @@ -56,16 +55,16 @@ const initialDialogShape = { ], }, ], - }), - [SDKTypes.SendActivity]: () => ({ + }, + [SDKTypes.SendActivity]: { activity: '', - }), - [SDKTypes.AttachmentInput]: () => initialInputDialog, - [SDKTypes.ChoiceInput]: () => initialInputDialog, - [SDKTypes.ConfirmInput]: () => initialInputDialog, - [SDKTypes.DateTimeInput]: () => initialInputDialog, - [SDKTypes.NumberInput]: () => initialInputDialog, - [SDKTypes.TextInput]: () => initialInputDialog, + }, + [SDKTypes.AttachmentInput]: initialInputDialog, + [SDKTypes.ChoiceInput]: initialInputDialog, + [SDKTypes.ConfirmInput]: initialInputDialog, + [SDKTypes.DateTimeInput]: initialInputDialog, + [SDKTypes.NumberInput]: initialInputDialog, + [SDKTypes.TextInput]: initialInputDialog, }; export function getNewDesigner(name: string, description: string) { @@ -138,8 +137,7 @@ export const deleteActions = ( export const seedNewDialog = ( $type: string, designerAttributes: Partial = {}, - optionalAttributes: object = {}, - seededActions: unknown[] = [] + optionalAttributes: object = {} ): object => { return { $type, @@ -148,7 +146,7 @@ export const seedNewDialog = ( ...designerAttributes, }, ...seedDefaults($type), - ...(initialDialogShape[$type] ? initialDialogShape[$type](seededActions) : {}), + ...(initialDialogShape[$type] || {}), ...optionalAttributes, }; }; From 0d89e4805b638d629c961f133dfff74f86a4590b Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 12 Mar 2020 10:48:02 +0800 Subject: [PATCH 5/6] initialDialogShape as a function --- .../packages/client/src/pages/design/index.tsx | 8 ++++---- .../packages/lib/shared/src/dialogFactory.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 7d13e55765..13a0c4c8b3 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -11,7 +11,7 @@ import formatMessage from 'format-message'; import { globalHistory } from '@reach/router'; import get from 'lodash/get'; import { PromptTab } from '@bfc/shared'; -import { seedNewDialog } from '@bfc/shared'; +import { seedNewDialog, SDKTypes } from '@bfc/shared'; import { DialogInfo } from '@bfc/indexers'; import set from 'lodash/set'; @@ -326,13 +326,13 @@ function DesignPage(props) { async function onSubmit(data: { name: string; description: string }) { const seededContent = seedNewDialog( - 'Microsoft.AdaptiveDialog', + SDKTypes.AdaptiveDialog, { name: data.name, description: data.description }, { generator: `${data.name}.lg`, - } + }, + state.actionsSeed || [] ); - set(seededContent, 'triggers[0].actions', state.actionsSeed || []); await actions.createDialog({ id: data.name, content: seededContent }); } diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 585b5a6aed..c241993427 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -23,15 +23,16 @@ const initialInputDialog = { }; const initialDialogShape = { - [SDKTypes.AdaptiveDialog]: { + [SDKTypes.AdaptiveDialog]: seededActions => ({ $type: SDKTypes.AdaptiveDialog, triggers: [ { $type: SDKTypes.OnBeginDialog, ...getNewDesigner('BeginDialog', ''), + actions: [...seededActions], }, ], - }, + }), [SDKTypes.OnConversationUpdateActivity]: { $type: 'Microsoft.OnConversationUpdateActivity', actions: [ @@ -134,10 +135,19 @@ export const deleteActions = ( return deleteAdaptiveActionList(inputs, deleteLgTemplates, deleteLuIntents); }; +const seedInitialDialogShape = ($type: string, seededParams?) => { + const shape = initialDialogShape[$type]; + if (typeof shape === 'function' && seededParams !== undefined) { + return shape(seededParams) || {}; + } + return shape || {}; +}; + export const seedNewDialog = ( $type: string, designerAttributes: Partial = {}, - optionalAttributes: object = {} + optionalAttributes: object = {}, + seededParams?: object ): object => { return { $type, @@ -146,7 +156,7 @@ export const seedNewDialog = ( ...designerAttributes, }, ...seedDefaults($type), - ...(initialDialogShape[$type] || {}), + ...seedInitialDialogShape($type, seededParams), ...optionalAttributes, }; }; From 2f3481a82d3de46ea6f5b1b9115293dd38ee3357 Mon Sep 17 00:00:00 2001 From: Chris Whitten Date: Thu, 12 Mar 2020 11:02:41 -0700 Subject: [PATCH 6/6] feat: visual/move with lgapi (#2258) * dump real lg content before paste them * implement lg resources walker * update lg walker api * split insertNodes from pasteNodes * fix tslint * change copyUtils ExtarnelAPI interface * migrate to new api format * create real lg template when pasting * renaming * update walkLgResources * create lgTemplates for moved actions * hack the debounce issue #2247 * Fix build * Fix lint Co-authored-by: Ze Ye Co-authored-by: zeye <2295905420@qq.com> --- .../client/src/pages/design/index.tsx | 1 - .../visual-designer/src/editors/ObiEditor.tsx | 78 +++++++++++++------ .../visual-designer/src/utils/jsonTracker.ts | 6 +- .../copyUtils/copyInputDialog.test.ts | 2 +- .../copyUtils/copySendActivity.test.ts | 2 +- .../__tests__/jestMocks/externalApiStub.ts | 2 +- .../lib/shared/src/copyUtils/ExternalApi.ts | 6 +- .../shared/src/copyUtils/copyInputDialog.ts | 10 +-- .../shared/src/copyUtils/copySendActivity.ts | 2 +- .../lib/shared/src/copyUtils/index.ts | 2 +- .../packages/lib/shared/src/dialogFactory.ts | 9 ++- .../shared/src/walkerUtils/walkLgResources.ts | 12 +-- 12 files changed, 82 insertions(+), 50 deletions(-) diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 13a0c4c8b3..c33bc0bf90 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -13,7 +13,6 @@ import get from 'lodash/get'; import { PromptTab } from '@bfc/shared'; import { seedNewDialog, SDKTypes } from '@bfc/shared'; import { DialogInfo } from '@bfc/indexers'; -import set from 'lodash/set'; import { VisualEditorAPI } from '../../messenger/FrameAPI'; import { TestController } from '../../TestController'; diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 9a3973e972..2edc1c71e9 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,8 +5,16 @@ import { jsx } from '@emotion/core'; import { useContext, FC, useEffect, useState, useRef } from 'react'; import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { SDKTypes } from '@bfc/shared' -import { deleteAction, deleteActions, LgTemplateRef, LgMetaData, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; +import { + deleteAction, + deleteActions, + LgTemplateRef, + LgMetaData, + seedNewDialog, + ExternalResourceHandlerAsync, + walkLgResourcesInActionList, +} from '@bfc/shared'; +import { SDKTypes } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -57,7 +65,7 @@ export const ObiEditor: FC = ({ removeLuIntent, } = useContext(NodeRendererContext); - const dereferenceLg: ExternalResourceCopyHandlerAsync = async ( + const dereferenceLg: ExternalResourceHandlerAsync = async ( actionId: string, actionData: any, lgFieldName: string, @@ -75,7 +83,7 @@ export const ObiEditor: FC = ({ return targetTemplate ? targetTemplate.body : lgText; }; - const buildLgReference: ExternalResourceCopyHandlerAsync = async (nodeId, data, fieldName, fieldText) => { + const buildLgReference: ExternalResourceHandlerAsync = async (nodeId, data, fieldName, fieldText) => { if (!fieldText) return ''; const newLgTemplateName = new LgMetaData(fieldName, nodeId).toString(); const newLgTemplateRefStr = new LgTemplateRef(newLgTemplateName).toString(); @@ -167,26 +175,48 @@ export const ObiEditor: FC = ({ // Using copy-paste-delete pattern here is safer than using cut-paste // since create new dialog may be cancelled or failed - const copiedActions = copyNodes(data, e.actionIds); - onCreateDialog(copiedActions).then(newDialog => { - // defense modal cancellation - if (!newDialog) return; - - // delete old actions (they are already moved to new dialog) - const deleteResult = deleteNodes(data, e.actionIds, nodes => - deleteActions(nodes, deleteLgTemplates, deleteLuIntents) - ); - - // insert a BeginDialog action points to newly created dialog - const indexes = e.actionIds[0].match(/^(.+)\[(\d+)\]$/); - if (indexes === null || indexes.length !== 3) return; - - const [, arrayPath, actionIndexStr] = indexes; - const startIndex = parseInt(actionIndexStr); - const placeholderAction = seedNewDialog(SDKTypes.BeginDialog, undefined, { dialog: newDialog }); - const insertResult = insertAction(deleteResult, arrayPath, startIndex, placeholderAction); - onChange(insertResult); - }); + copyNodes(data, e.actionIds, dereferenceLg) + .then(copiedActions => { + const lgTemplatesToBeCreated: { name: string; body: string }[] = []; + walkLgResourcesInActionList(copiedActions, (designerId, actionData, fieldName, lgStr) => { + if (!lgStr) return ''; + + const lgName = new LgMetaData(fieldName, designerId).toString(); + const refString = new LgTemplateRef(lgName).toString(); + + lgTemplatesToBeCreated.push({ name: lgName, body: lgStr }); + actionData[fieldName] = refString; + return refString; + }); + return onCreateDialog(copiedActions).then(dialogName => ({ dialogName, lgTemplatesToBeCreated })); + }) + .then(async ({ dialogName: newDialog, lgTemplatesToBeCreated }) => { + // defense modal cancellation + if (!newDialog) return; + + // create lg templates for actions in new dialog + for (const { name, body } of lgTemplatesToBeCreated) { + await updateLgTemplate(newDialog, name, body); + } + + // delete old actions (they are already moved to new dialog) + + // HACK: https://github.com/microsoft/BotFramework-Composer/issues/2247 + const postponedDeleteLgTemplates = templates => setTimeout(() => deleteLgTemplates(templates), 501); + const deleteResult = deleteNodes(data, e.actionIds, nodes => + deleteActions(nodes, postponedDeleteLgTemplates, deleteLuIntents) + ); + + // insert a BeginDialog action points to newly created dialog + const indexes = e.actionIds[0].match(/^(.+)\[(\d+)\]$/); + if (indexes === null || indexes.length !== 3) return; + + const [, arrayPath, actionIndexStr] = indexes; + const startIndex = parseInt(actionIndexStr); + const placeholderAction = seedNewDialog(SDKTypes.BeginDialog, undefined, { dialog: newDialog }); + const insertResult = insertAction(deleteResult, arrayPath, startIndex, placeholderAction); + onChange(insertResult); + }); onFocusSteps([]); }; break; diff --git a/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/jsonTracker.ts index 06669e2040..587beb1ddf 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, ExternalResourceCopyHandlerAsync } from '@bfc/shared'; +import { seedNewDialog, deepCopyActions, generateSDKTitle, ExternalResourceHandlerAsync } from '@bfc/shared'; function parseSelector(path: string): null | string[] { if (!path) return null; @@ -172,7 +172,7 @@ export function insertAction(inputDialog, arrayPath: string, position: number, n return dialog; } -type DereferenceLgHandler = ExternalResourceCopyHandlerAsync; +type DereferenceLgHandler = ExternalResourceHandlerAsync; export async function copyNodes(inputDialog, nodeIds: string[], dereferenceLg: DereferenceLgHandler): Promise { const nodes = nodeIds.map(id => queryNode(inputDialog, id)).filter(x => x !== null); @@ -228,7 +228,7 @@ export async function pasteNodes( arrayPath: string, arrayIndex: number, clipboardNodes: any[], - handleLgField: ExternalResourceCopyHandlerAsync + handleLgField: ExternalResourceHandlerAsync ) { // Considering a scenario that copy one time but paste multiple times, // it requires seeding all $designer.id again by invoking deepCopy. diff --git a/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts b/Composer/packages/lib/shared/__tests__/copyUtils/copyInputDialog.test.ts index 1e0dd78bfd..fd4bd5193a 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: (id, data, field, value) => Promise.resolve(value + '(copy)'), + transformLgField: (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 12c7682f89..60ff581e56 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: (id, data, fieldName, fieldValue) => Promise.resolve(fieldValue + '(copy)'), + transformLgField: (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 7aa455f302..2b75fa995a 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: (id, data, fieldName, fieldValue) => Promise.resolve(fieldValue || ''), + transformLgField: (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 fe586e5924..04fcff71e1 100644 --- a/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts +++ b/Composer/packages/lib/shared/src/copyUtils/ExternalApi.ts @@ -3,14 +3,14 @@ import { DesignerData } from '../types'; -export type ExternalResourceCopyHandler = ( +export type ExternalResourceHandler = ( actionId: string, actionData: any, resourceFieldName: string, resourceValue?: CopiedType ) => CopiedType; -export type ExternalResourceCopyHandlerAsync = ( +export type ExternalResourceHandlerAsync = ( actionId: string, actionData: any, resourceFieldName: string, @@ -19,5 +19,5 @@ export type ExternalResourceCopyHandlerAsync = ( export interface ExternalApi { getDesignerId: (data?: DesignerData) => DesignerData; - copyLgTemplate: ExternalResourceCopyHandlerAsync; + transformLgField: ExternalResourceHandlerAsync; } diff --git a/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts index 71eda21774..c6b0705337 100644 --- a/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts +++ b/Composer/packages/lib/shared/src/copyUtils/copyInputDialog.ts @@ -9,22 +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]); + const transform = (data, fieldName: string) => externalApi.transformLgField(nodeId, data, fieldName, data[fieldName]); if (input.prompt !== undefined) { - copy.prompt = await copyLgField(copy, 'prompt'); + copy.prompt = await transform(copy, 'prompt'); } if (input.unrecognizedPrompt !== undefined) { - copy.unrecognizedPrompt = await copyLgField(copy, 'unrecognizedPrompt'); + copy.unrecognizedPrompt = await transform(copy, 'unrecognizedPrompt'); } if (input.invalidPrompt !== undefined) { - copy.invalidPrompt = await copyLgField(copy, 'invalidPrompt'); + copy.invalidPrompt = await transform(copy, 'invalidPrompt'); } if (input.defaultValueResponse !== undefined) { - copy.defaultValueResponse = await copyLgField(copy, 'defaultValueResponse'); + copy.defaultValueResponse = await transform(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 929201c322..2a8e4fade9 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(nodeId, copy, 'activity', copy.activity); + copy.activity = await externalApi.transformLgField(nodeId, copy, 'activity', copy.activity); } return copy; diff --git a/Composer/packages/lib/shared/src/copyUtils/index.ts b/Composer/packages/lib/shared/src/copyUtils/index.ts index d6bd0088a7..b645488c68 100644 --- a/Composer/packages/lib/shared/src/copyUtils/index.ts +++ b/Composer/packages/lib/shared/src/copyUtils/index.ts @@ -2,4 +2,4 @@ // Licensed under the MIT License. export { copyAdaptiveAction } from './copyAdaptiveAction'; -export { ExternalResourceCopyHandlerAsync } from './ExternalApi'; +export { ExternalResourceHandlerAsync } from './ExternalApi'; diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index f823da827e..59f94669d9 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -9,7 +9,8 @@ import { copyAdaptiveAction } from './copyUtils'; import { deleteAdaptiveAction, deleteAdaptiveActionList } from './deleteUtils'; import { MicrosoftIDialog } from './types'; import { SDKTypes } from './types'; -import { ExternalResourceCopyHandlerAsync } from './copyUtils/ExternalApi'; +import { ExternalResourceHandlerAsync } from './copyUtils/ExternalApi'; + interface DesignerAttributes { name: string; description: string; @@ -110,14 +111,14 @@ export const seedDefaults = (type: string) => { return assignDefaults(properties); }; -export const deepCopyAction = async (data, copyLgTemplate: ExternalResourceCopyHandlerAsync) => { +export const deepCopyAction = async (data, copyLgTemplate: ExternalResourceHandlerAsync) => { return await copyAdaptiveAction(data, { getDesignerId, - copyLgTemplate, + transformLgField: copyLgTemplate, }); }; -export const deepCopyActions = async (actions: any[], copyLgTemplate: ExternalResourceCopyHandlerAsync) => { +export const deepCopyActions = async (actions: any[], copyLgTemplate: ExternalResourceHandlerAsync) => { // 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. diff --git a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts index faf75eb066..3ce6ae9e5d 100644 --- a/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts +++ b/Composer/packages/lib/shared/src/walkerUtils/walkLgResources.ts @@ -1,18 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import get from 'lodash/get'; +import { ExternalResourceHandler } from '../copyUtils/ExternalApi'; import { walkAdaptiveAction } from '../deleteUtils/walkAdaptiveAction'; -import { SDKTypes } from '../types'; import { walkAdaptiveActionList } from '../deleteUtils/walkAdaptiveActionList'; +import { SDKTypes } from '../types'; -type LgFieldHandler = (action, lgFieldName: string, lgString: string) => any; +type LgFieldHandler = ExternalResourceHandler; const findLgFields = (action: any, handleLgField: LgFieldHandler) => { if (typeof action === 'string') return; if (!action || !action.$type) return; const onFound = (fieldName: string) => { - action[fieldName] && handleLgField(action, fieldName, action[fieldName]); + action[fieldName] && handleLgField(get(action, '$designer.id'), action, fieldName, action[fieldName]); }; switch (action.$type) { @@ -37,6 +39,6 @@ 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)); +export const walkLgResourcesInActionList = (actionList: any[], handleLgResource: LgFieldHandler) => { + walkAdaptiveActionList(actionList, action => findLgFields(action, handleLgResource)); };