diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 87ee086f21..d5dad318a0 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,8 +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 has from 'lodash/has'; -import get from 'lodash/get'; +import { deleteAction, deleteActions } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -48,6 +47,19 @@ export const ObiEditor: FC = ({ const { focusedId, focusedEvent, clipboardActions, copyLgTemplate, removeLgTemplates } = useContext( NodeRendererContext ); + + const deleteLgTemplates = (lgTemplates: string[]) => { + const lgPattern = /\[(bfd\w+-\d+)\]/; + const normalizedLgTemplates = lgTemplates + .map(x => { + const matches = lgPattern.exec(x); + if (matches && matches.length === 2) return matches[1]; + return ''; + }) + .filter(x => !!x); + return removeLgTemplates('common', normalizedLgTemplates); + }; + const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => { let handler; switch (eventName) { @@ -69,37 +81,7 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.Delete: handler = e => { - // TODO: move the shared logic into shared lib as a generic destruction process - const findLgTemplates = (value: any): string[] => { - const targetNames = ['prompt', 'unrecognizedPrompt', 'defaultValueResponse', 'invalidPrompt', 'activity']; - const targets: string[] = []; - - targetNames.forEach(name => { - if (has(value, name)) { - targets.push(get(value, name)); - } - }); - - const templates: string[] = []; - targets.forEach(target => { - // only match auto generated lg temapte name - const reg = /\[(bfd((?:activity)|(?:prompt)|(?:unrecognizedPrompt)|(?:defaultValueResponse)|(?:invalidPrompt))-\d{6})\]/g; - let matchResult; - while ((matchResult = reg.exec(target)) !== null) { - const templateName = matchResult[1]; - templates.push(templateName); - } - }); - - return templates; - }; - - const cleanLgTemplate = async (removedData: any): Promise => { - const templateNames: string[] = findLgTemplates(removedData); - const lgFileId = 'common'; - await removeLgTemplates(lgFileId, templateNames); - }; - onChange(deleteNode(data, e.id, cleanLgTemplate)); + onChange(deleteNode(data, e.id, node => deleteAction(node, deleteLgTemplates))); onFocusSteps([]); }; break; @@ -153,7 +135,7 @@ export const ObiEditor: FC = ({ break; case NodeEventTypes.DeleteSelection: handler = e => { - const dialog = deleteNodes(data, e.actionIds); + const dialog = deleteNodes(data, e.actionIds, nodes => deleteActions(nodes, deleteLgTemplates)); onChange(dialog); onFocusSteps([]); }; diff --git a/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveAction.test.ts b/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveAction.test.ts new file mode 100644 index 0000000000..f293d13356 --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveAction.test.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { walkAdaptiveAction } from '../../src/deleteUtils/walkAdaptiveAction'; + +describe('walkAdaptiveAction', () => { + it('can walk single action', () => { + const action = { + $type: 'Microsoft.SendActivity', + activity: 'hello', + }; + const spy = jest.fn(); + walkAdaptiveAction(action, x => spy(x)); + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(action); + }); +}); diff --git a/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveActionList.test.ts b/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveActionList.test.ts new file mode 100644 index 0000000000..6852722d4d --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/deleteUtils/walkAdaptiveActionList.test.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { walkAdaptiveActionList } from '../../src/deleteUtils/walkAdaptiveActionList'; + +describe('walkAdaptiveAction', () => { + it('can walk action list', () => { + const actions = [ + { + $type: 'Microsoft.SendActivity', + prompt: 'hello', + }, + { + $type: 'Microsoft.ChoiceInput', + prompt: 'hello', + }, + ]; + const spy = jest.fn(); + walkAdaptiveActionList(actions, x => spy(x)); + + expect(spy).toBeCalledTimes(2); + expect(spy).toHaveBeenNthCalledWith(1, actions[0]); + expect(spy).toHaveBeenNthCalledWith(2, actions[1]); + }); +}); diff --git a/Composer/packages/lib/shared/src/deleteUtils/AdaptiveActionVisitor.ts b/Composer/packages/lib/shared/src/deleteUtils/AdaptiveActionVisitor.ts new file mode 100644 index 0000000000..7390c56fb8 --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/AdaptiveActionVisitor.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { MicrosoftIDialog } from '../types'; + +export type AdaptiveActionVisitor = (action: MicrosoftIDialog) => void; diff --git a/Composer/packages/lib/shared/src/deleteUtils/index.ts b/Composer/packages/lib/shared/src/deleteUtils/index.ts new file mode 100644 index 0000000000..0c94f765c0 --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/index.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { MicrosoftIDialog, SDKTypes } from '../types'; + +import { walkAdaptiveAction } from './walkAdaptiveAction'; +import { walkAdaptiveActionList } from './walkAdaptiveActionList'; + +const collectLgTemplates = (action: any, outputTemplates: string[]) => { + if (typeof action === 'string') return; + if (!action || !action.$type) return; + + switch (action.$type) { + case SDKTypes.SendActivity: + outputTemplates.push(action.activity); + break; + case SDKTypes.AttachmentInput: + case SDKTypes.ChoiceInput: + case SDKTypes.ConfirmInput: + case SDKTypes.DateTimeInput: + case SDKTypes.NumberInput: + case SDKTypes.TextInput: + outputTemplates.push(action.prompt, action.unrecognizedPrompt, action.invalidPrompt, action.defaultValueResponse); + break; + } +}; + +export const deleteAdaptiveAction = (data: MicrosoftIDialog, deleteLgTemplates: (lgTemplates: string[]) => any) => { + const lgTemplates: string[] = []; + walkAdaptiveAction(data, action => collectLgTemplates(action, lgTemplates)); + + deleteLgTemplates(lgTemplates.filter(activity => !!activity)); +}; + +export const deleteAdaptiveActionList = ( + data: MicrosoftIDialog[], + deleteLgTemplates: (lgTemplates: string[]) => any +) => { + const lgTemplates: string[] = []; + walkAdaptiveActionList(data, action => collectLgTemplates(action, lgTemplates)); + + deleteLgTemplates(lgTemplates.filter(activity => !!activity)); +}; diff --git a/Composer/packages/lib/shared/src/deleteUtils/walkActionWithChildren.ts b/Composer/packages/lib/shared/src/deleteUtils/walkActionWithChildren.ts new file mode 100644 index 0000000000..a3d64fd90c --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/walkActionWithChildren.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { AdaptiveActionVisitor } from './AdaptiveActionVisitor'; +import { walkAdaptiveActionList } from './walkAdaptiveActionList'; + +export const walkActionWithChildren = (input, visitor: AdaptiveActionVisitor) => { + visitor(input); + + walkAdaptiveActionList(input.actions, visitor); +}; diff --git a/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveAction.ts b/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveAction.ts new file mode 100644 index 0000000000..cd032f7fde --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveAction.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { MicrosoftIDialog, SDKTypes } from '../types'; + +import { walkIfCondition } from './walkIfCondition'; +import { walkSwitchCondition } from './walkSwitchCondition'; +import { walkActionWithChildren } from './walkActionWithChildren'; +import { AdaptiveActionVisitor } from './AdaptiveActionVisitor'; + +const WalkerMap: { [$type: string]: (input, visitor: AdaptiveActionVisitor) => void } = { + [SDKTypes.IfCondition]: walkIfCondition, + [SDKTypes.SwitchCondition]: walkSwitchCondition, + [SDKTypes.Foreach]: walkActionWithChildren, + [SDKTypes.ForeachPage]: walkActionWithChildren, + [SDKTypes.EditActions]: walkActionWithChildren, +}; + +export const walkAdaptiveAction = (input, visit: (action: MicrosoftIDialog) => void): void => { + if (typeof input === 'string') { + visit(input); + return; + } + + if (!input || !input.$type) { + return; + } + + if (WalkerMap[input.$type]) { + WalkerMap[input.$type](input, visit); + } else { + visit(input); + } + return; +}; diff --git a/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveActionList.ts b/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveActionList.ts new file mode 100644 index 0000000000..6359116f94 --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/walkAdaptiveActionList.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { MicrosoftIDialog } from '../types'; + +import { walkAdaptiveAction } from './walkAdaptiveAction'; +export const walkAdaptiveActionList = (inputs: MicrosoftIDialog[], visit: (action: MicrosoftIDialog) => void): void => { + if (Array.isArray(inputs)) { + inputs.forEach(action => walkAdaptiveAction(action, visit)); + } +}; diff --git a/Composer/packages/lib/shared/src/deleteUtils/walkIfCondition.ts b/Composer/packages/lib/shared/src/deleteUtils/walkIfCondition.ts new file mode 100644 index 0000000000..cacf194040 --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/walkIfCondition.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { AdaptiveActionVisitor } from './AdaptiveActionVisitor'; +import { walkAdaptiveActionList } from './walkAdaptiveActionList'; + +export const walkIfCondition = (input, visitor: AdaptiveActionVisitor) => { + visitor(input); + + walkAdaptiveActionList(input.actions, visitor); + walkAdaptiveActionList(input.elseActions, visitor); +}; diff --git a/Composer/packages/lib/shared/src/deleteUtils/walkSwitchCondition.ts b/Composer/packages/lib/shared/src/deleteUtils/walkSwitchCondition.ts new file mode 100644 index 0000000000..c31f86ee1a --- /dev/null +++ b/Composer/packages/lib/shared/src/deleteUtils/walkSwitchCondition.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { AdaptiveActionVisitor } from './AdaptiveActionVisitor'; +import { walkAdaptiveActionList } from './walkAdaptiveActionList'; + +export const walkSwitchCondition = (input, visitor: AdaptiveActionVisitor) => { + visitor(input); + + walkAdaptiveActionList(input.default, visitor); + + if (Array.isArray(input.cases)) { + input.cases.forEach(currentCase => { + walkAdaptiveActionList(currentCase.actions, visitor); + }); + } +}; diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index d7b814612f..8a2dfd21c5 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -6,6 +6,8 @@ import nanoid from 'nanoid/generate'; import { DesignerData } from './types/sdk'; import { appschema } from './appschema'; import { copyAdaptiveAction } from './copyUtils'; +import { deleteAdaptiveAction, deleteAdaptiveActionList } from './deleteUtils'; +import { MicrosoftIDialog } from './types'; interface DesignerAttributes { name: string; @@ -98,6 +100,14 @@ export const deepCopyAction = async ( }); }; +export const deleteAction = (data: MicrosoftIDialog, deleteLgTemplates: (templates: string[]) => any) => { + return deleteAdaptiveAction(data, deleteLgTemplates); +}; + +export const deleteActions = (inputs: MicrosoftIDialog[], deleteLgTemplates: (templates: string[]) => any) => { + return deleteAdaptiveActionList(inputs, deleteLgTemplates); +}; + export const seedNewDialog = ( $type: string, designerAttributes: Partial = {},