-
Notifications
You must be signed in to change notification settings - Fork 374
[WIP] LG activity copy behavior: copies all possible lg templates #1193
Conversation
a-b-r-o-w-n
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please refer to my comments on #1096. It seems that was merged before my review was posted.
|
I will work on Andy's comments today, #1196 & #1193 both of them. As for the data mutation problem, I want to solve it in another PR (also open an issue before solving it) by leveraging the JsonWalk handler @boydc2014 mentioned, @a-b-r-o-w-n what's your opinion on that? |
I know you use copy then manipulate the data for a reason, it would save some visiting counts and in this context you only touch the filed. But as I said, data-manipulation when walk\travel\iterate is a very obvious anti-pattern, very easily to raise alert for reviewers, and you will need efforts to explain and justify, and maybe more efforts to prevent other contribute to follow this. You can just copy JsonWalk into this shared/utils, without worrying about the server side, that's OK, i can clean up the server side to leverage the same one. But that's not the core design issue we are discussing here, it's that: can we avoid this anti-pattern easily? can we turn it into a object construction process by walking on another read-only object? |
|
Agreed, we can involve more changes in this PR to make the copy utils in a good pattern @boydc2014 |
b0f644f to
a0a3733
Compare
Actually a copy pattern don't even have to use JsonWalk, (jsonWalk is more suitable for your override approach) A copy pattern can be simply achieved as a simple recursive bottom-up copy. // define a map of copy functions, for example
async CopySendAcitivity(value, shellAPI) {
// a pure function return a new value, the input is read-only
return createNewAcitity(value, shellApi)
}
// High-order function design for IfCondition, SwitchElse
function CopyContainerObject([childrenProperties]) =>
return
async CopyIfConditon(value, shellAPI) {
let result = clone(value); // Optimization, don't clone children properties
foreach (property in childrenProperites) {
result[property] = [];
foreach (idx in value[property])
result result[idx] = await copyAction(value[property][idx]) // note here, reuse the copy action
}
}
}
}
const copyMap = {
'Type.SendActivity': CopySendAcitivity
'Type.IfCondition': CopyConatinerAction(['actions', 'elseActions'])
}
// search copy function, or fallback to lodash
function async copyAction(action, shellAPI)
{
if (value.type && value.type in copyMap) {
return await copyMap[value.type](value, shellApi);
} else {
return = lodash.clone(value);
}
}
} |
|
I like @boydc2014 proposed approach. |
|
Let me also show JsonWalk works with copy construtor @yeze322 just for the purpose of illustration. You can compare this approach with the previous recusively copy // section 1, first you don't have to add "yielder" because visitor is already a very complete abstraction, if we want async feature a simple async version is async on visitor.
const jsonWalk = async (path, value, visitor) {
...
await visitor(path, value);
...
.... await jsonWalk()
}
// section 2 def a copy map like the previous approach
async CopySendAcitivity(value, shellAPI) {
// a pure function return a new value, the input is read-only
return createNewAcitity(value, shellApi)
}
const copyMap = {
'Type.SendActivity': CopySendAcitivity
}
// section 3, def a visitor and walk
function copyAction(action, shellAPI) {
let result = {};
const visitor = (path, value) => {
if (value.type and value.type) {
// if there is a copy constructor defined, let it take over
jsonPath.set(result, path, await copyMap[value.type](value, shellApi);
return true;
} else {
// other wise fallback
jsonPath.set(result, path, lodash.clone(value));
return false;
}
}
return await jsonWalk('$', action, visitor);
}So some points, to compare this to the above one
Anyway, no matter a generic jsonWalk or a simple recursively solution, i think i showed the principle that,
To me, the first approach is good enough |
Me too, this solution looks like a compromise solution between mutation & json walk and is easy to be migrated to. The most significant benifit here is we can clearly manage how each 3 pass! happy ending |
| return copyLgTemplate(id, templateName, newTemplateName, { | ||
| getLgTemplates: shellApi.getLgTemplates, | ||
| updateLgTemplate: shellApi.updateLgTemplate, | ||
| }); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| @@ -0,0 +1,42 @@ | |||
| const TEMPLATE_PATTERN = /^\[(bfd.+-\d+)\]$/; | |||
There was a problem hiding this comment.
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.
| const { getLgTemplates, updateLgTemplate } = lgApi; | ||
| if (!getLgTemplates) return templateNameToCopy; | ||
|
|
||
| let rawLg: any[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be typed?
|
|
||
| let rawLg: any[] = []; | ||
| try { | ||
| rawLg = await getLgTemplates(lgFileName, templateNameToCopy); |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes!
|
|
||
| const overrideLgActivity = async (data, { copyLgTemplate }) => { | ||
| const newLgId = `[bfdactivity-${data.$designer.id}]`; | ||
| data.activity = await copyLgTemplate(data.activity, newLgId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be safer to try/catch this and assign some other value.
| } | ||
| }; | ||
|
|
||
| // TODO: use $type from SDKTypes (after solving circular import issue). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😞 This has to do with the way typescript does enums at runtime.
| const DEFAULT_CHILDREN_KEYS = [NestedFieldNames.Actions]; | ||
| const childrenMap = { | ||
| ['Microsoft.IfCondition']: [NestedFieldNames.Actions, NestedFieldNames.ElseActions], | ||
| ['Microsoft.SwitchCondition']: [NestedFieldNames.Cases, NestedFieldNames.DefaultCase], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll want to include EditActions as well.
|
This PR requires longer time than we expected and may not meet the timeline. Keep it open for tracking those comments but split it into smaller PRs. Plan:
|
|
Want to start a discussion about how to leverage the same walker on both deleting & copying. @boydc2014 @a-b-r-o-w-n The two scenarios are:
I think ideally they could and should share the same walker, but seems the solution we chose is not suitable for it. I'm considering the second solution Dong proposed: Under this solution, we implement three parts:
Here is the interface. We've got enough implementation details so omit them // Walker
function walker(path: string, input: AdaptiveAction, visitor: Function) {
visitor(path, input);
// traverse it.
.....
}
// Function map
const handlerMap = {
'Microsoft.IfCondition': {
copy: () => {....},
delete: () => {....},
}
}
// Copy
const result = {}
function copyVisitor(path: string, input) {
const copy = handlerMap(input.$type).copy(input);
set(result, path, copy)
}
// Delete
function deleteVisitor(path: string, input) {
handlerMap(input.$type).delete(input);
}What's your opinion? |
We don't have to share too much of between copy and delete. Because from the copy code, you can see, there are only 1 place inside "CopyContainerAction" which did a very simple foreach on property. So, it's totally OK to me that, we register another deleteContainerAction. It would looks very clean, the only dup is just a foreach on property. |
|
We had a discussion offline |
will follow up this, we can provide more methods like: |
addressed in #1512, add: |

Description
Issues:
Not all LG fields will be copied when copying an action. We should consider covering several kinds of LG activities:
Resolves missed comments in Handle copy LG activity in visual editor #1096
Changes:
copyLgTemplatehandler's implementation from 'copyUtils.ts' in 'shared' lib to Shell (lgHandlers.ts)copyLgTemplatethrough ExtensionContainer to visual editorRemaining issue:
updateLgTemplateis not concurrency-saferemoveLgTemplateis not concurrency-safe(addressed in LG api inside composer client is not concurrency-safe #1210 )
Task Item
fixes #1191
fixes #1213
Type of change
Please delete options that are not relevant.
Checklist
Screenshots
Please include screenshots or gifs if your PR include UX changes.