Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f080966
copy other lg fields
yeze322 Oct 22, 2019
be0202c
optmize
yeze322 Oct 22, 2019
88af3c7
Merge branch 'master' into zeye/fix-lg-copy
boydc2014 Oct 23, 2019
4b0b084
extract lgUtils out of copyUtils
yeze322 Oct 23, 2019
2f90434
update lg template regex
yeze322 Oct 23, 2019
ccb4763
refine copyLgActivity API
yeze322 Oct 23, 2019
fbb44ca
add type for copyLgTemplate
yeze322 Oct 23, 2019
92b2fed
move lgUtils to Shell
yeze322 Oct 23, 2019
9eb46a5
fix lgUtils return value format
yeze322 Oct 23, 2019
e12fcd6
rename input ap name to copyLgTemplate
yeze322 Oct 23, 2019
b9ce7b3
fix lgUtils test
yeze322 Oct 23, 2019
9bc39d0
pass down copyLgTemplate from Shell instead of in shared lib
yeze322 Oct 23, 2019
67e2dbd
rename lgUtils -> lgHandlers
yeze322 Oct 23, 2019
5f72af0
fix the format of newTemplateName
yeze322 Oct 23, 2019
a0a3733
change test name of lgHandlers
yeze322 Oct 23, 2019
237cb0c
group imports in ExtensionContainer
yeze322 Oct 23, 2019
79e470d
Merge branch 'master' into zeye/fix-lg-copy
yeze322 Oct 23, 2019
eabf7ce
update several ts types
yeze322 Oct 23, 2019
b2497a5
use lodash.cloneDeep in copyUtils
yeze322 Oct 23, 2019
237f6a1
move walker to another file
yeze322 Oct 23, 2019
bb62953
move copyUtils and walk to 'utils' folder
yeze322 Oct 23, 2019
e279a71
move copyUtils.test to sub folder
yeze322 Oct 23, 2019
d5094f0
add UT for 'walkAdaptiveAction'
yeze322 Oct 23, 2019
ce00b7b
fix async version walk + UT
yeze322 Oct 23, 2019
9462253
Merge branch 'master' into zeye/fix-lg-copy
boydc2014 Oct 23, 2019
91658bf
Merge branch 'master' into zeye/fix-lg-copy
boydc2014 Oct 23, 2019
2e2f050
Merge branch 'master' into zeye/fix-lg-copy
boydc2014 Oct 24, 2019
7ceec4e
Merge branch 'master' into zeye/fix-lg-copy
yeze322 Oct 25, 2019
db2016a
Merge branch 'master' into zeye/fix-lg-copy
boydc2014 Oct 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { isLgActivity, copyLgTemplate } from '../../../src/store/action/lgHandlers';

describe('lgHandlers', () => {
describe('#isLgActivity', () => {
it('can handle empty input', () => {
expect(isLgActivity('')).toBeFalsy();
});

it('can identify correct templates', () => {
expect(isLgActivity('[bfdactivity-1234]')).toBeTruthy();
expect(isLgActivity('[bfdprompt-1234]')).toBeTruthy();
expect(isLgActivity('[bfdinvalidPrompt-1234]')).toBeTruthy();
expect(isLgActivity('[bfdunrecognizedPrompt-1234]')).toBeTruthy();
});

it('can identify invalid templates', () => {
expect(isLgActivity('any string')).toBeFalsy();

expect(isLgActivity('bfdactivity-1234')).toBeFalsy();
expect(isLgActivity('[bfdactivity-1234')).toBeFalsy();
expect(isLgActivity('bfdactivity-1234')).toBeFalsy();
expect(isLgActivity('[bfdactivity-abc]')).toBeFalsy();

expect(isLgActivity('[abfdactivity-1234]')).toBeFalsy();
expect(isLgActivity('[abfdactivity]')).toBeFalsy();
expect(isLgActivity('[bfdactivity-]')).toBeFalsy();
});
});

describe('#copyLgTemplate', () => {
const lgApi = {
getLgTemplates: (id, activity) => Promise.resolve([{ Name: 'bfdactivity-1234', Body: 'Hello' }]),
updateLgTemplate: (id, newId, newContent) => Promise.resolve(true),
};

it('can skip invalid input params', async () => {
expect(await copyLgTemplate('common', '', 'newId', lgApi)).toEqual('');
expect(await copyLgTemplate('common', 'wrong', 'newId', lgApi)).toEqual('wrong');
expect(await copyLgTemplate('common', 'wrong', 'newId', null as any)).toEqual('wrong');
});

it('can copy existing template to a new template', async () => {
expect(await copyLgTemplate('common', '[bfdactivity-1234]', '[bfdactivity-5678]', lgApi)).toEqual(
'[bfdactivity-5678]'
);
});

it('can handle non-existing template', async () => {
expect(await copyLgTemplate('common', '[bfdactivity-4321]', '[bfdactivity-5678]', lgApi)).toEqual(
'[bfdactivity-4321]'
);
});

it('can recover from API error', async () => {
// getLgTemplates error
expect(
await copyLgTemplate('common', '[bfdactivity-1234]', 'bfdactivity-5678', {
...lgApi,
getLgTemplates: () => Promise.reject(),
})
).toEqual('[bfdactivity-1234]');

expect(
await copyLgTemplate('common', '[bfdactivity-1234]', 'bfdactivity-5678', {
...lgApi,
updateLgTemplate: () => Promise.reject(),
})
).toEqual('Hello');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { initializeIcons } from 'office-ui-fabric-react';
import { LuFile, ShellData } from 'shared';

import { copyLgTemplate } from '../store/action/lgHandlers';
import ApiClient from '../messenger/ApiClient';

import getEditor from './EditorMap';
Expand Down Expand Up @@ -95,6 +96,13 @@ const shellApi = {
});
},

copyLgTemplate: (id: string, templateName: string, newTemplateName: string) => {
return copyLgTemplate(id, templateName, newTemplateName, {
getLgTemplates: shellApi.getLgTemplates,
updateLgTemplate: shellApi.updateLgTemplate,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be an api call? Also, you are importing copyLgTemplate from the actions, but it does not conform to the action interface. This looks like a code smell to me.

Maybe copyLgTemplate is a shared utility, not an action? Also, passing in the get and update functions seem like a smell. Why is this dependency injection needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with you, copyLgTemplate should be an API but it's not provided by server. So I simulated an API here.

},

createDialog: () => {
return apiClient.apiCall('createDialog');
},
Expand Down
42 changes: 42 additions & 0 deletions Composer/packages/client/src/store/action/lgHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const TEMPLATE_PATTERN = /^\[(bfd.+-\d+)\]$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file does not belong with the other actions.

export function isLgActivity(activity: string) {
return activity && TEMPLATE_PATTERN.test(activity);
}

export async function copyLgTemplate(
lgFileName: string,
templateNameToCopy: string,
newTemplateName: string,
lgApi: {
getLgTemplates: (lgFileName: string, templateName: string) => Promise<any>;
updateLgTemplate: (lgFileName: string, newTemplateName: string, newContent: string) => Promise<any>;
}
): Promise<string> {
if (!templateNameToCopy) return '';
if (!isLgActivity(templateNameToCopy) || !lgApi) return templateNameToCopy;

const { getLgTemplates, updateLgTemplate } = lgApi;
if (!getLgTemplates) return templateNameToCopy;

let rawLg: any[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be typed?

try {
rawLg = await getLgTemplates(lgFileName, templateNameToCopy);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need to add getLgTemplateByName to the lg api. We do this kind of thing in a few places!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes!

} catch (error) {
return templateNameToCopy;
}

const currentLg = rawLg.find(lg => `[${lg.Name}]` === templateNameToCopy);

if (currentLg) {
// Create new lg activity.
const newLgContent = currentLg.Body;
try {
const newTemplateId = (TEMPLATE_PATTERN.exec(newTemplateName) || [])[1];
await updateLgTemplate(lgFileName, newTemplateId, newLgContent);
return newTemplateName;
} catch (e) {
return newLgContent;
}
}
return templateNameToCopy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,12 @@ export const ObiEditor: FC<ObiEditorProps> = ({
}): JSX.Element | null => {
let divRef;

const { focusedId, focusedEvent, updateLgTemplate, getLgTemplates, removeLgTemplate } = useContext(
NodeRendererContext
);
const { focusedId, focusedEvent, removeLgTemplate, copyLgTemplate } = useContext(NodeRendererContext);
const [clipboardContext, setClipboardContext] = useState({
clipboardActions: [],
setClipboardActions: actions => setClipboardContext({ ...clipboardContext, clipboardActions: actions }),
});

const lgApi = { getLgTemplates, removeLgTemplate, updateLgTemplate };
const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => {
let handler;
switch (eventName) {
Expand Down Expand Up @@ -108,7 +105,12 @@ export const ObiEditor: FC<ObiEditorProps> = ({
case NodeEventTypes.Insert:
if (eventData.$type === 'PASTE') {
handler = e => {
pasteNodes(data, e.id, e.position, clipboardContext.clipboardActions, lgApi).then(dialog => {
// Bind 'common' as lg template file name to align with 'removeLgTemplate' usage.
const externalCopyApi = {
copyLgTemplate: (templateNameToCopy: string, newTemplateName: string) =>
copyLgTemplate('common', templateNameToCopy, newTemplateName),
};
pasteNodes(data, e.id, e.position, clipboardContext.clipboardActions, externalCopyApi).then(dialog => {
onChange(dialog);
});
};
Expand Down
2 changes: 2 additions & 0 deletions Composer/packages/extensions/visual-designer/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({
updateLgTemplate,
getLgTemplates,
removeLgTemplate,
copyLgTemplate,
undo,
redo,
} = shellApi;
Expand All @@ -62,6 +63,7 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({
updateLgTemplate: updateLgTemplate,
getLgTemplates: getLgTemplates,
removeLgTemplate: removeLgTemplate,
copyLgTemplate: copyLgTemplate,
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { string } from 'prop-types';

interface LgTemplate {
Name: string;
Expand All @@ -12,5 +11,6 @@ export const NodeRendererContext = React.createContext({
focusedTab: '',
getLgTemplates: (_id: string, _templateName: string) => Promise.resolve([] as LgTemplate[]),
removeLgTemplate: (_id: string, _templateName: string) => Promise.resolve(),
updateLgTemplate: (_id: string, _templateName: string, _template: string) => Promise.resolve('' as string),
updateLgTemplate: (_id: string, _templateName: string, _template: string) => Promise.resolve(''),
copyLgTemplate: (_id: string, _templateName: string, _newTemplateName: string) => Promise.resolve(''),
});
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,15 @@ export function appendNodesAfter(inputDialog, targetId, newNodes) {
return dialog;
}

export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, lgApi) {
export async function pasteNodes(
inputDialog: any,
arrayPath: string,
arrayIndex: number,
newNodes: any[],
externalApi: {
copyLgTemplate: (srcTemplateName: string, newTemplateName: string) => Promise<string>;
}
) {
if (!Array.isArray(newNodes) || newNodes.length === 0) {
return inputDialog;
}
Expand All @@ -211,7 +219,7 @@ export async function pasteNodes(inputDialog, arrayPath, arrayIndex, newNodes, l
// Deep copy nodes with external resources
const copiedNodes = await Promise.all(
newNodes.map(async x => {
const node = await deepCopyAction(x, lgApi);
const node = await deepCopyAction(x, externalApi);
return node;
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
import { copyAdaptiveAction } from '../src/copyUtils';
import { copyAdaptiveAction } from '../../src/utils/copyUtils';

describe('copyAdaptiveAction', () => {
const lgTemplate = [{ Name: 'bfdactivity-1234', Body: '-hello' }];
const externalApi = {
updateDesigner: data => {
data.$designer = { id: '5678' };
},
lgApi: {
getLgTemplates: (fileId, activityId) => {
return Promise.resolve(lgTemplate);
},
updateLgTemplate: (filedId, activityId, activityBody) => {
return Promise.resolve(true);
},
},
};
const externalApiWithFailure = {
...externalApi,
lgApi: {
...externalApi.lgApi,
updateLgTemplate: () => Promise.reject(),
},
copyLgTemplate: (templateToCopy: string, newTemplateName: string) => Promise.resolve(newTemplateName),
};

it('should return {} when input is invalid', async () => {
Expand Down Expand Up @@ -53,12 +38,6 @@ describe('copyAdaptiveAction', () => {
$designer: { id: '5678' },
activity: '[bfdactivity-5678]',
});

expect(await copyAdaptiveAction(sendActivity, externalApiWithFailure)).toEqual({
$type: 'Microsoft.SendActivity',
$designer: { id: '5678' },
activity: '-hello',
});
});

it('can copy TextInput', async () => {
Expand All @@ -72,7 +51,8 @@ describe('copyAdaptiveAction', () => {
alwaysPrompt: false,
allowInterruptions: 'true',
outputFormat: 'none',
prompt: '[bfdactivity-1234]',
prompt: '[bfdprompt-1234]',
invalidPrompt: '[bfdinvalidPrompt-1234]',
};

expect(await copyAdaptiveAction(promptText, externalApi)).toEqual({
Expand All @@ -84,19 +64,8 @@ describe('copyAdaptiveAction', () => {
alwaysPrompt: false,
allowInterruptions: 'true',
outputFormat: 'none',
prompt: '[bfdactivity-5678]',
});

expect(await copyAdaptiveAction(promptText, externalApiWithFailure)).toEqual({
$type: 'Microsoft.TextInput',
$designer: {
id: '5678',
},
maxTurnCount: 3,
alwaysPrompt: false,
allowInterruptions: 'true',
outputFormat: 'none',
prompt: '-hello',
prompt: '[bfdprompt-5678]',
invalidPrompt: '[bfdinvalidPrompt-5678]',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { walkAdaptiveAction } from '../../src/utils/walkAdaptiveAction';

describe('#walkAdaptiveAction', () => {
const action = {
$type: 'Microsoft.IfCondition',
actions: [{ $type: 'Microsoft.SendActivity' }],
elseActions: [
{
$type: 'Microsoft.Foreach',
actions: [{ $type: 'Microsoft.SendActivity' }],
},
],
};

it('can walk every child Adaptive Action node', async () => {
let actionCount = 0;
const counter: any = () => {
actionCount += 1;
};
await walkAdaptiveAction(action, counter);
expect(actionCount).toEqual(4);
});

it('can walk every child Adaptive Action node async', async () => {
let actionCount = 0;
const counter = () => {
return new Promise(resolve => {
setTimeout(() => {
actionCount += 1;
resolve();
}, 10);
});
};

await walkAdaptiveAction(action, counter);
expect(actionCount).toEqual(4);
});
});
Loading