## Description
Replaces the old boring action selector dropdown with a much more
sophisticated UI that is capable of going above and beyond. Users with
an aversion to code can now build their more complex workflows with a
click of a few buttons.
Consider this code snippet
```javascript
Api1.run(() => {
showAlert("Hello");
navigateTo('Page1', {}, 'SAME_WINDOW');
}, () => {
removeValue("test");
});
```
|**Old action selector** |**New action selector**|
|:-:|:-:|
|<img width="250" alt="Screenshot 2023-03-29 at 16 54 14"
src="https://user-images.githubusercontent.com/32433245/228520661-a639b580-8986-4aec-a0f5-e2786d1a0f56.png">|
<img width="250" alt="Screenshot 2023-03-29 at 16 55 15"
src="https://user-images.githubusercontent.com/32433245/228521043-5025aa42-af95-4574-b586-bc4c721240bc.png">|
**Click on an action block to edit its parameters.**
<img width="500" alt="Screenshot 2023-03-29 at 17 01 18"
src="https://user-images.githubusercontent.com/32433245/228522479-493769d0-9d2c-4b67-b493-a79e3bb9c947.png">
**Switch to JS mode to get the raw code**
<img width="273" alt="Screenshot 2023-03-29 at 17 05 51"
src="https://user-images.githubusercontent.com/32433245/228523458-13bc0302-4c94-4176-b5aa-3ec208122f57.png">
### Code changes
**New UI components**
- ActionCreator component splits the code into block statements.
- Each block statement is represented by ActionTree.tsx UI component.
- ActionTree.tsx represents an action and its chains.
- ActionCard.tsx is the block that represents the individual action on
the UI.
- ActionSelector.tsx component is popover that contains the form for
editing individual action.
- TabView, TextView, SelectorView, ActionSelectorView and KeyValueView
are components that represent configurable fields in ActionSelector
form.
**AST methods**
- Added methods to get/set function names, expressions, arguments.
- Added methods to get/set then/catch blocks to allow chaining of
actions.
- Added methods to check if code is convertible to UI.
Fixes #10160
Fixes #21588
Fixes #21392
Fixes #21393
Fixes #7903
Fixes #15895
Fixes #17765
Fixes #14562
Depends on https://github.com/appsmithorg/design-system/pull/306
## Type of change
- New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Manual
- Jest
- Cypress
### Test Plan
https://github.com/appsmithorg/TestSmith/issues/2296
### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag
### QA activity:
- [x] Test plan has been approved by relevant developers
- [x] Test plan has been peer reviewed by QA
- [x] Cypress test cases have been added and approved by either SDET or
manual QA
- [x] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [x] Added Test Plan Approved label after reveiwing all Cypress test
---------
Co-authored-by: Rimil Dey <rimil@appsmith.com>
Co-authored-by: arunvjn <arun@appsmith.com>
Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
Co-authored-by: Parthvi Goswami <parthvigoswami@Parthvis-MacBook-Pro.local>
248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
//check difference for after body change and parsing
|
|
import type { JSCollection, JSAction, Variable } from "entities/JSCollection";
|
|
import { ENTITY_TYPE } from "entities/AppsmithConsole";
|
|
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
|
import AppsmithConsole from "utils/AppsmithConsole";
|
|
import { isEmpty, isEqual, xorWith } from "lodash";
|
|
|
|
export type ParsedJSSubAction = {
|
|
name: string;
|
|
body: string;
|
|
arguments: Array<Variable>;
|
|
isAsync: boolean;
|
|
// parsedFunction - used only to determine if function is async
|
|
parsedFunction?: () => unknown;
|
|
};
|
|
|
|
export type ParsedBody = {
|
|
actions: Array<ParsedJSSubAction>;
|
|
variables: Array<Variable>;
|
|
};
|
|
|
|
export type JSUpdate = {
|
|
id: string;
|
|
parsedBody: ParsedBody | undefined;
|
|
};
|
|
|
|
export const getDifferenceInJSArgumentArrays = (x: any, y: any) =>
|
|
isEmpty(xorWith(x, y, isEqual));
|
|
|
|
export const getDifferenceInJSCollection = (
|
|
parsedBody: ParsedBody,
|
|
jsAction: JSCollection,
|
|
) => {
|
|
const newActions: ParsedJSSubAction[] = [];
|
|
const toBearchivedActions: JSAction[] = [];
|
|
const toBeUpdatedActions: JSAction[] = [];
|
|
const nameChangedActions = [];
|
|
const toBeAddedActions: Partial<JSAction>[] = [];
|
|
//check if body is changed and update if exists or
|
|
// add to new array so it can be added to main collection
|
|
if (parsedBody.actions && parsedBody.actions.length > 0) {
|
|
for (let i = 0; i < parsedBody.actions.length; i++) {
|
|
const action = parsedBody.actions[i];
|
|
const preExisted = jsAction.actions.find((js) => js.name === action.name);
|
|
if (preExisted) {
|
|
if (
|
|
preExisted.actionConfiguration.body !== action.body ||
|
|
preExisted.actionConfiguration.isAsync !== action.isAsync ||
|
|
!getDifferenceInJSArgumentArrays(
|
|
preExisted.actionConfiguration.jsArguments,
|
|
action.arguments,
|
|
)
|
|
) {
|
|
toBeUpdatedActions.push({
|
|
...preExisted,
|
|
actionConfiguration: {
|
|
...preExisted.actionConfiguration,
|
|
body: action.body,
|
|
jsArguments: action.arguments,
|
|
isAsync: action.isAsync,
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
newActions.push(action);
|
|
}
|
|
}
|
|
}
|
|
//create deleted action list
|
|
if (jsAction.actions && jsAction.actions.length > 0 && parsedBody.actions) {
|
|
for (let i = 0; i < jsAction.actions.length; i++) {
|
|
const preAction = jsAction.actions[i];
|
|
const existed = parsedBody.actions.find(
|
|
(js: ParsedJSSubAction) => js.name === preAction.name,
|
|
);
|
|
if (!existed) {
|
|
toBearchivedActions.push(preAction);
|
|
}
|
|
}
|
|
}
|
|
//check if new is name changed from deleted actions
|
|
if (toBearchivedActions.length && newActions.length) {
|
|
for (let i = 0; i < newActions.length; i++) {
|
|
const nameChange = toBearchivedActions.find(
|
|
(js) => js.actionConfiguration.body === newActions[i].body,
|
|
);
|
|
if (nameChange) {
|
|
const updateExisting = jsAction.actions.find(
|
|
(js) => js.id === nameChange.id,
|
|
);
|
|
if (updateExisting) {
|
|
const indexOfArchived = toBearchivedActions.findIndex((js) => {
|
|
js.id === updateExisting.id;
|
|
});
|
|
//will be part of new nameChangedActions for now
|
|
toBeUpdatedActions.push({
|
|
...updateExisting,
|
|
name: newActions[i].name,
|
|
});
|
|
nameChangedActions.push({
|
|
id: updateExisting.id,
|
|
collectionId: updateExisting.collectionId,
|
|
oldName: updateExisting.name,
|
|
newName: newActions[i].name,
|
|
pageId: updateExisting.pageId,
|
|
});
|
|
newActions.splice(i, 1);
|
|
toBearchivedActions.splice(indexOfArchived, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newActions.length > 0) {
|
|
for (let i = 0; i < newActions.length; i++) {
|
|
const action = newActions[i];
|
|
const obj = {
|
|
name: action.name,
|
|
collectionId: jsAction.id,
|
|
executeOnLoad: false,
|
|
pageId: jsAction.pageId,
|
|
workspaceId: jsAction.workspaceId,
|
|
actionConfiguration: {
|
|
body: action.body,
|
|
isAsync: action.isAsync,
|
|
timeoutInMillisecond: 0,
|
|
jsArguments: action.arguments || [],
|
|
},
|
|
};
|
|
toBeAddedActions.push(obj);
|
|
}
|
|
}
|
|
if (toBearchivedActions.length > 0) {
|
|
for (let i = 0; i < toBearchivedActions.length; i++) {
|
|
const action = toBearchivedActions[i];
|
|
const deleteArchived = jsAction.actions.findIndex((js) => {
|
|
action.id === js.id;
|
|
});
|
|
jsAction.actions.splice(deleteArchived, 1);
|
|
}
|
|
}
|
|
//change in variables
|
|
const varList = jsAction.variables;
|
|
let changedVariables: Array<Variable> = [];
|
|
if (parsedBody.variables.length) {
|
|
for (let i = 0; i < parsedBody.variables.length; i++) {
|
|
const newVar = parsedBody.variables[i];
|
|
const existedVar = varList.find((item) => item.name === newVar.name);
|
|
if (!!existedVar) {
|
|
const existedValue = existedVar.value;
|
|
if (
|
|
(!!existedValue &&
|
|
existedValue.toString() !==
|
|
(newVar.value && newVar.value.toString())) ||
|
|
(!existedValue && !!newVar.value)
|
|
) {
|
|
changedVariables.push(newVar);
|
|
}
|
|
} else {
|
|
changedVariables.push(newVar);
|
|
}
|
|
}
|
|
} else {
|
|
changedVariables = jsAction.variables;
|
|
}
|
|
//delete variable
|
|
if (varList && varList.length > 0 && parsedBody.variables) {
|
|
for (let i = 0; i < varList.length; i++) {
|
|
const preVar = varList[i];
|
|
const existed = parsedBody.variables.find(
|
|
(jsVar: Variable) => jsVar.name === preVar.name,
|
|
);
|
|
if (!existed) {
|
|
const newvarList = varList.filter(
|
|
(deletedVar) => deletedVar.name !== preVar.name,
|
|
);
|
|
changedVariables = changedVariables.concat(newvarList);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
newActions: toBeAddedActions,
|
|
updateActions: toBeUpdatedActions,
|
|
deletedActions: toBearchivedActions,
|
|
nameChangedActions: nameChangedActions,
|
|
changedVariables: changedVariables,
|
|
};
|
|
};
|
|
|
|
export const pushLogsForObjectUpdate = (
|
|
actions: Partial<JSAction>[],
|
|
jsCollection: JSCollection,
|
|
text: string,
|
|
) => {
|
|
for (let i = 0; i < actions.length; i++) {
|
|
AppsmithConsole.info({
|
|
logType: LOG_TYPE.JS_ACTION_UPDATE,
|
|
text: text,
|
|
source: {
|
|
type: ENTITY_TYPE.JSACTION,
|
|
name: jsCollection.name + "." + actions[i].name,
|
|
id: jsCollection.id,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
export const createDummyJSCollectionActions = (
|
|
pageId: string,
|
|
workspaceId: string,
|
|
) => {
|
|
const body =
|
|
"export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\tmyFun1 () {\n\t\t//\twrite code here\n\t\t//\tthis.myVar1 = [1,2,3]\n\t},\n\tasync myFun2 () {\n\t\t//\tuse async-await or promises\n\t\t//\tawait storeValue('varName', 'hello world')\n\t}\n}";
|
|
|
|
const actions = [
|
|
{
|
|
name: "myFun1",
|
|
pageId,
|
|
workspaceId,
|
|
executeOnLoad: false,
|
|
actionConfiguration: {
|
|
body: "function (){\n\t\t//\twrite code here\n\t\t//\tthis.myVar1 = [1,2,3]\n\t}",
|
|
isAsync: false,
|
|
timeoutInMillisecond: 0,
|
|
jsArguments: [],
|
|
},
|
|
clientSideExecution: true,
|
|
},
|
|
{
|
|
name: "myFun2",
|
|
pageId,
|
|
workspaceId,
|
|
executeOnLoad: false,
|
|
actionConfiguration: {
|
|
body: "async function () {\n\t\t//\tuse async-await or promises\n\t\t//\tawait storeValue('varName', 'hello world')\n\t}",
|
|
isAsync: true,
|
|
timeoutInMillisecond: 0,
|
|
jsArguments: [],
|
|
},
|
|
clientSideExecution: true,
|
|
},
|
|
];
|
|
return {
|
|
actions,
|
|
body,
|
|
};
|
|
};
|