feat: Settings js editor (#9984)

* POC

* Closing channels

* WIP

* v1

* get working with JS editor

* autocomplete

* added comments

* try removing an import

* different way of import

* dependency map added to body

* triggers can be part of js editor functions hence

* removed unwanted lines

* new flow chnages

* Resolve conflicts

* small css changes for empty state

* Fix prettier

* Fixes

* flow changes part 2

* Mock web worker for testing

* Throw errors during evaluation

* Action execution should be non blocking on the main thread to evaluation of further actions

* WIP

* Fix build issue

* Fix warnings

* Rename

* Refactor and add tests for worker util

* Fix response flow post refactor

* added settings icon for js editor

* WIP

* WIP

* WIP

* Tests for promises

* settings for each function of js object added

* Error handling

* Error handing action validation

* Update test

* Passing callback data in the eval trigger flow

* log triggers to be executed

* WIP

* confirm before execution

* Remove debugging

* Fix backwards compatibility

* Avoid passing trigger meta around

* fix button loading

* handle error callbacks

* fix tests

* tests

* fix console error when checking for async

* Fix async function check

* Fix async function check again

* fix bad commit

* Add some comments

* added clientSideExecution flag for js functions

* css changes for settings icon

* unsued code removed

* on page load PART 1

* onPageLoad rest iof changes

* corrected async badge

* removed duplicate test cases

* added confirm modal for js functions

* removed unused code

* small chnage

* dependency was not getting created

* Fix confirmation modal

* unused code removed

* replaced new confirmsaga

* confirmaton box changes

* Fixing JSEditor Run butn locator

* corrected property

* dependency map was failing

* changed key for confirmation box

Co-authored-by: hetunandu <hetu@appsmith.com>
Co-authored-by: Hetu Nandu <hetunandu@gmail.com>
Co-authored-by: Arpit Mohan <arpit@appsmith.com>
Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Apeksha Bhosale 2022-03-17 17:35:17 +05:30 committed by GitHub
parent 00570de1bc
commit 79e165af96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 700 additions and 121 deletions

View File

@ -6,10 +6,10 @@ const agHelper = new AggregateHelper();
const locator = new CommonLocators();
export class JSEditor {
private _runButton = "//li//*[local-name() = 'svg' and @class='run-button']/parent::li"
private _outputConsole = ".CodeEditorTarget"
private _jsObjName = ".t--js-action-name-edit-field span"
private _jsObjTxt = ".t--js-action-name-edit-field input"
private _runButton = "//li//*[local-name() = 'svg' and @class='run-button']";
private _outputConsole = ".CodeEditorTarget";
private _jsObjName = ".t--js-action-name-edit-field span";
private _jsObjTxt = ".t--js-action-name-edit-field input";
private _newJSobj = "span:contains('New JS Object')"
private _bindingsClose = ".t--entity-property-close"

View File

@ -45,6 +45,7 @@ export const EVALUATE_REDUX_ACTIONS = [
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS,
ReduxActionTypes.EXECUTE_JS_FUNCTION_SUCCESS,
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,

View File

@ -1,6 +1,6 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { JSCollection, JSAction } from "entities/JSCollection";
import { RefactorAction } from "api/JSActionAPI";
import { RefactorAction, SetFunctionPropertyPayload } from "api/JSActionAPI";
export const createNewJSCollection = (
pageId: string,
): ReduxAction<{ pageId: string }> => ({
@ -61,3 +61,17 @@ export const executeJSFunction = (payload: {
payload,
};
};
export const updateFunctionProperty = (payload: SetFunctionPropertyPayload) => {
return {
type: ReduxActionTypes.SET_FUNCTION_PROPERTY,
payload,
};
};
export const updateJSFunction = (payload: SetFunctionPropertyPayload) => {
return {
type: ReduxActionTypes.UPDATE_JS_FUNCTION_PROPERTY_INIT,
payload,
};
};

View File

@ -279,6 +279,20 @@ export const setActionsToExecuteOnPageLoad = (
};
};
export const setJSActionsToExecuteOnPageLoad = (
actions: Array<{
executeOnLoad: boolean;
id: string;
name: string;
collectionId?: string;
}>,
) => {
return {
type: ReduxActionTypes.SET_JS_ACTION_TO_EXECUTE_ON_PAGELOAD,
payload: actions,
};
};
export const bindDataOnCanvas = (payload: {
queryId: string;
applicationId: string;

View File

@ -32,6 +32,11 @@ export interface CreateJSCollectionRequest {
pluginType: PluginType;
}
export type SetFunctionPropertyPayload = {
action: JSAction;
propertyName: string;
value: any;
};
export interface RefactorAction {
pageId: string;
actionId: string;

View File

@ -58,6 +58,7 @@ export interface SavePageResponse extends ApiResponse {
executeOnLoad: boolean;
id: string;
name: string;
collectionId?: string;
}>;
};
}

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.64268 3.03595L5.90731 1.3999H8.09268L8.35731 3.03595C8.92037 3.21109 9.42751 3.49525 9.86401 3.85804L11.4774 3.23537L12.6 5.02199L11.2555 6.07606C11.3262 6.37368 11.3665 6.68187 11.3665 6.9999C11.3665 7.31793 11.3262 7.62611 11.2555 7.92373L12.6 8.97781L11.4774 10.7644L9.86401 10.1418C9.42751 10.5046 8.92037 10.7887 8.35731 10.9639L8.09268 12.5999H5.90731L5.64268 10.9639C5.0796 10.7887 4.57248 10.5046 4.13598 10.1418L2.52255 10.7644L1.39999 8.97781L2.7445 7.92373C2.67379 7.62611 2.63354 7.31793 2.63354 6.9999C2.63354 6.68187 2.67379 6.37368 2.7445 6.07606L1.39999 5.02199L2.52255 3.23537L4.13598 3.85804C4.57249 3.49525 5.0796 3.21109 5.64268 3.03595ZM9.09999 6.9999C9.09999 5.84011 8.1598 4.8999 6.99999 4.8999C5.8402 4.8999 4.89999 5.84011 4.89999 6.9999C4.89999 8.15969 5.8402 9.0999 6.99999 9.0999C8.1598 9.0999 9.09999 8.15969 9.09999 6.9999Z" fill="#F86A2B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -450,6 +450,15 @@ export const JS_EXECUTION_SUCCESS = () => "JS Function executed successfully";
export const JS_EXECUTION_FAILURE = () => "JS Function execution failed";
export const JS_EXECUTION_FAILURE_TOASTER = () =>
"There was an error while executing function";
export const JS_SETTINGS_ONPAGELOAD = () => "Run Function on Page load";
export const JS_SETTINGS_ONPAGELOAD_SUBTEXT = () =>
"Will refresh data every time page is reloaded";
export const JS_SETTINGS_CONFIRM_EXECUTION = () =>
"Request confirmation before calling Function?";
export const JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT = () =>
"Ask confirmation from the user every time before refreshing data";
export const JS_SETTINGS_EXECUTE_TIMEOUT = () =>
"Function Timeout (in milliseconds)";
// Import/Export Application features
export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import application";

View File

@ -40,6 +40,8 @@ import { setCurrentTab } from "actions/debuggerActions";
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
import EntityBottomTabs from "./EntityBottomTabs";
import Icon from "components/ads/Icon";
import { ReactComponent as FunctionSettings } from "assets/icons/menu/settings.svg";
import JSFunctionSettings from "pages/Editor/JSEditor/JSFunctionSettings";
import FlagBadge from "components/utils/FlagBadge";
const ResponseContainer = styled.div`
@ -94,9 +96,16 @@ const ResponseTabAction = styled.li`
display: inline-block;
flex: 1;
}
.function-actions {
margin-left: auto;
order: 2;
svg {
display: inline-block;
}
}
.run-button {
margin: 0 15px;
margin-left: 10px;
margin-right: 15px;
}
&.active {
background-color: #f0f0f0;
@ -192,6 +201,18 @@ function JSResponseView(props: Props) {
dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB));
}, []);
const [openSettings, setOpenSettings] = useState(false);
const [selectedFunction, setSelectedFunction] = useState<
undefined | JSAction
>(undefined);
const isSelectedFunctionAsync = (id: string) => {
const jsAction = jsObject.actions.find((action) => action.id === id);
if (!!jsAction) {
return jsAction?.actionConfiguration.isAsync;
}
return false;
};
const tabs = [
{
key: "body",
@ -232,17 +253,35 @@ function JSResponseView(props: Props) {
}
key={action.id}
onClick={() => {
runAction(action);
setSelectActionId(action.id);
}}
>
<JSFunction />{" "}
<div className="function-name">{action.name}</div>
{action.actionConfiguration.isAsync ? (
<FlagBadge name={"ASYNC"} />
) : (
""
)}
<RunFunction className="run-button" />
<div className="function-actions">
{action.actionConfiguration.isAsync ? (
<FlagBadge name={"ASYNC"} />
) : (
""
)}
{isSelectedFunctionAsync(action.id) ? (
<FunctionSettings
onClick={() => {
setSelectedFunction(action);
setOpenSettings(true);
}}
/>
) : (
""
)}
<RunFunction
className="run-button"
onClick={() => {
runAction(action);
}}
/>
</div>
</ResponseTabAction>
);
})}
@ -271,6 +310,17 @@ function JSResponseView(props: Props) {
/>
)}
</ResponseViewer>
{openSettings &&
!!selectedFunction &&
isSelectedFunctionAsync(selectedFunction.id) && (
<JSFunctionSettings
action={selectedFunction}
openSettings={openSettings}
toggleSettings={() => {
setOpenSettings(!openSettings);
}}
/>
)}
</>
)}
</ResponseTabWrapper>

View File

@ -8,6 +8,7 @@ const Flag = styled.span`
text-transform: uppercase;
font-size: 10px;
font-weight: 600;
margin-right: 10px;
`;
function FlagBadge(props: { name: string }) {

View File

@ -19,6 +19,9 @@ export type ExecutionResult = {
export type TriggerSource = {
id: string;
name: string;
collectionId?: string;
isJSAction?: boolean;
actionId?: string;
};
export type ExecuteTriggerPayload = {
@ -107,6 +110,8 @@ export interface PageAction {
name: string;
jsonPathKeys: string[];
timeoutInMillisecond: number;
clientSideExecution?: boolean;
collectionId?: string;
}
export interface ExecuteErrorPayload extends ErrorActionPayload {

View File

@ -640,6 +640,13 @@ export const ReduxActionTypes = {
UPDATE_JS_ACTION_BODY_INIT: "UPDATE_JS_ACTION_BODY_INIT",
UPDATE_JS_ACTION_BODY_SUCCESS: "UPDATE_JS_ACTION_BODY_SUCCESS",
SEND_TEST_EMAIL: "SEND_TEST_EMAIL",
SET_FUNCTION_PROPERTY: "SET_FUNCTION_PROPERTY",
UPDATE_JS_FUNCTION_PROPERTY_INIT: "UPDATE_JS_FUNCTION_PROPERTY_INIT",
UPDATE_JS_FUNCTION_PROPERTY_SUCCESS: "UPDATE_JS_FUNCTION_PROPERTY_SUCCESS",
TOGGLE_FUNCTION_EXECUTE_ON_LOAD_INIT: "TOGGLE_FUNCTION_EXECUTE_ON_LOAD_INIT",
TOGGLE_FUNCTION_EXECUTE_ON_LOAD_SUCCESS:
"TOGGLE_FUNCTION_EXECUTE_ON_LOAD_SUCCESS",
SET_JS_ACTION_TO_EXECUTE_ON_PAGELOAD: "SET_JS_ACTION_TO_EXECUTE_ON_PAGELOAD",
ENABLE_GUIDED_TOUR: "ENABLE_GUIDED_TOUR",
GUIDED_TOUR_MARK_STEP_COMPLETED: "GUIDED_TOUR_MARK_STEP_COMPLETED",
SET_CURRENT_STEP: "SET_CURRENT_STEP",
@ -838,6 +845,7 @@ export const ReduxActionErrorTypes = {
FETCH_RELEASES_ERROR: "FETCH_RELEASES_ERROR",
RESTART_SERVER_ERROR: "RESTART_SERVER_ERROR",
UPDATE_JS_ACTION_BODY_ERROR: "UPDATE_JS_ACTION_BODY_ERROR",
UPDATE_JS_FUNCTION_PROPERTY_ERROR: "UPDATE_JS_FUNCTION_PROPERTY_ERROR",
DELETE_ORG_ERROR: "DELETE_ORG_ERROR",
REFLOW_BETA_FLAGS_INIT_ERROR: "REFLOW_BETA_FLAGS_INIT_ERROR",
GET_ALL_TEMPLATES_ERROR: "GET_ALL_TEMPLATES_ERROR",

View File

@ -17,6 +17,7 @@ export enum ActionTriggerType {
GET_CURRENT_LOCATION = "GET_CURRENT_LOCATION",
WATCH_CURRENT_LOCATION = "WATCH_CURRENT_LOCATION",
STOP_WATCHING_CURRENT_LOCATION = "STOP_WATCHING_CURRENT_LOCATION",
CONFIRMATION_MODAL = "CONFIRMATION_MODAL",
}
export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
@ -35,6 +36,7 @@ export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
[ActionTriggerType.GET_CURRENT_LOCATION]: "getCurrentLocation",
[ActionTriggerType.WATCH_CURRENT_LOCATION]: "watchLocation",
[ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION]: "stopWatch",
[ActionTriggerType.CONFIRMATION_MODAL]: "ConfirmationModal",
};
export type RunPluginActionDescription = {
@ -158,6 +160,11 @@ export type StopWatchingCurrentLocationDescription = {
payload?: Record<string, never>;
};
export type ConfirmationModal = {
type: ActionTriggerType.CONFIRMATION_MODAL;
payload?: Record<string, any>;
};
export type ActionDescription =
| RunPluginActionDescription
| ClearPluginActionDescription
@ -173,4 +180,5 @@ export type ActionDescription =
| ClearIntervalDescription
| GetCurrentLocationDescription
| WatchCurrentLocationDescription
| StopWatchingCurrentLocationDescription;
| StopWatchingCurrentLocationDescription
| ConfirmationModal;

View File

@ -81,6 +81,8 @@ export interface DataTreeJSAction {
export interface MetaArgs {
arguments: Variable[];
isAsync: boolean;
confirmBeforeExecute: boolean;
}
/**
* Map of overriding property as key and overridden property as values

View File

@ -31,15 +31,21 @@ export const generateDataTreeJSAction = (
const dependencyMap: DependencyMap = {};
dependencyMap["body"] = [];
const actions = js.config.actions;
const actionsData: Record<string, any> = {};
if (actions) {
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
meta[action.name] = {
arguments: action.actionConfiguration.jsArguments,
isAsync: action.actionConfiguration.isAsync,
confirmBeforeExecute: !!action.confirmBeforeExecute,
};
bindingPaths[action.name] = EvaluationSubstitutionType.SMART_SUBSTITUTE;
dynamicBindingPathList.push({ key: action.name });
dependencyMap["body"].push(action.name);
actionsData[action.name] = {
data: (js.data && js.data[`${action.id}`]) || {},
};
}
}
return {
@ -54,5 +60,6 @@ export const generateDataTreeJSAction = (
dynamicBindingPathList: dynamicBindingPathList,
variables: listVariables,
dependencyMap: dependencyMap,
...actionsData,
};
};

View File

@ -21,9 +21,10 @@ export interface JSCollection {
export interface JSActionConfig {
body: string;
isAsync: boolean;
timeoutInMilliseconds: number;
timeoutInMillisecond: number;
jsArguments: Array<Variable>;
}
export interface JSAction extends BaseAction {
actionConfiguration: JSActionConfig;
clientSideExecution: boolean;
}

View File

@ -0,0 +1,79 @@
import React from "react";
import styled from "styled-components";
import Checkbox from "components/ads/Checkbox";
import Dialog from "components/ads/DialogComponent";
import { JSAction } from "entities/JSCollection";
import { updateFunctionProperty } from "actions/jsPaneActions";
import { useDispatch } from "react-redux";
import {
createMessage,
JS_SETTINGS_ONPAGELOAD,
JS_SETTINGS_ONPAGELOAD_SUBTEXT,
JS_SETTINGS_CONFIRM_EXECUTION,
JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT,
} from "@appsmith/constants/messages";
const FormRow = styled.div`
margin-bottom: ${(props) => props.theme.spaces[10] + 1}px;
&.flex {
display: flex;
align-items: center;
.cs-text {
margin-right: 30px;
color: rgb(9, 7, 7);
}
}
`;
interface JSFunctionSettingsProps {
action: JSAction;
openSettings: boolean;
toggleSettings: () => void;
}
function JSFunctionSettings(props: JSFunctionSettingsProps) {
const { action } = props;
const dispatch = useDispatch();
const updateProperty = (value: boolean | number, propertyName: string) => {
dispatch(
updateFunctionProperty({
action: props.action,
propertyName: propertyName,
value: value,
}),
);
};
return (
<Dialog
canOutsideClickClose
isOpen={props.openSettings}
onClose={props.toggleSettings}
title={`Function settings - ${props.action.name}`}
>
<FormRow>
<Checkbox
fill={false}
info={createMessage(JS_SETTINGS_ONPAGELOAD_SUBTEXT)}
isDefaultChecked={action.executeOnLoad}
label={createMessage(JS_SETTINGS_ONPAGELOAD)}
onCheckChange={(value: boolean) =>
updateProperty(value, "executeOnLoad")
}
/>
</FormRow>
<FormRow>
<Checkbox
fill={false}
info={createMessage(JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT)}
isDefaultChecked={action.confirmBeforeExecute}
label={createMessage(JS_SETTINGS_CONFIRM_EXECUTION)}
onCheckChange={(value: boolean) =>
updateProperty(value, "confirmBeforeExecute")
}
/>
</FormRow>
</Dialog>
);
}
export default JSFunctionSettings;

View File

@ -5,10 +5,10 @@ import {
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { keyBy } from "lodash";
import { set, keyBy } from "lodash";
import produce from "immer";
const initialState: JSCollectionDataState = [];
export interface JSCollectionData {
isLoading: boolean;
config: JSCollection;
@ -64,8 +64,9 @@ const jsActionsReducer = createReducer(initialState, {
action: ReduxAction<{ data: JSCollection }>,
): JSCollectionDataState =>
state.map((a) => {
if (a.config.id === action.payload.data.id)
return { isLoading: false, config: action.payload.data };
if (a.config.id === action.payload.data.id) {
return { ...a, isLoading: false, config: action.payload.data };
}
return a;
}),
[ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS]: (
@ -75,6 +76,7 @@ const jsActionsReducer = createReducer(initialState, {
state.map((a) => {
if (a.config.id === action.payload.data.id)
return {
...a,
isLoading: false,
config: action.payload.data,
};
@ -286,6 +288,71 @@ const jsActionsReducer = createReducer(initialState, {
}
return a;
}),
[ReduxActionTypes.UPDATE_JS_FUNCTION_PROPERTY_SUCCESS]: (
state: JSCollectionDataState,
action: ReduxAction<{ collection: JSCollection }>,
): JSCollectionDataState =>
state.map((a) => {
if (a.config.id === action.payload.collection.id) {
return {
...a,
data: action.payload,
};
}
return a;
}),
[ReduxActionTypes.TOGGLE_FUNCTION_EXECUTE_ON_LOAD_SUCCESS]: (
state: JSCollectionDataState,
action: ReduxAction<{
actionId: string;
collectionId: string;
executeOnLoad: boolean;
}>,
): JSCollectionDataState =>
state.map((a) => {
if (a.config.id === action.payload.collectionId) {
const updatedActions = a.config.actions.map((jsAction) => {
if (jsAction.id === action.payload.actionId) {
set(jsAction, `executeOnLoad`, action.payload.executeOnLoad);
}
return jsAction;
});
return {
...a,
config: {
...a.config,
actions: updatedActions,
},
};
}
return a;
}),
[ReduxActionTypes.SET_JS_ACTION_TO_EXECUTE_ON_PAGELOAD]: (
state: JSCollectionDataState,
action: ReduxAction<
Array<{
executeOnLoad: boolean;
id: string;
name: string;
collectionId: string;
}>
>,
) => {
return produce(state, (draft) => {
const CollectionUpdateSearch = keyBy(action.payload, "collectionId");
const actionUpdateSearch = keyBy(action.payload, "id");
draft.forEach((action, index) => {
if (action.config.id in CollectionUpdateSearch) {
const allActions = draft[index].config.actions;
allActions.forEach((js) => {
if (js.id in actionUpdateSearch) {
js.executeOnLoad = actionUpdateSearch[js.id].executeOnLoad;
}
});
}
});
});
},
});
export default jsActionsReducer;

View File

@ -38,11 +38,14 @@ import {
clearIntervalSaga,
setIntervalSaga,
} from "sagas/ActionExecution/SetIntervalSaga";
import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils";
import {
getCurrentLocationSaga,
stopWatchCurrentLocation,
watchCurrentLocation,
} from "sagas/ActionExecution/GetCurrentLocationSaga";
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
import { ModalType } from "reducers/uiReducers/modalActionReducer";
export type TriggerMeta = {
source?: TriggerSource;
@ -125,6 +128,17 @@ export function* executeActionTriggers(
case ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION:
response = yield call(stopWatchCurrentLocation, eventType, triggerMeta);
break;
case ActionTriggerType.CONFIRMATION_MODAL:
const payloadInfo = {
name: trigger?.payload?.funName,
modalOpen: true,
modalType: ModalType.RUN_ACTION,
};
const flag = yield call(requestModalConfirmationSaga, payloadInfo);
if (!flag) {
throw new UserCancelledActionExecutionError();
}
break;
default:
log.error("Trigger type unknown", trigger);
throw Error("Trigger type unknown");

View File

@ -23,6 +23,7 @@ import {
getCurrentPageNameByActionId,
isActionDirty,
isActionSaving,
getJSCollection,
} from "selectors/entitiesSelector";
import {
getAppMode,
@ -89,6 +90,8 @@ import {
UserCancelledActionExecutionError,
} from "sagas/ActionExecution/errorUtils";
import { trimQueryString } from "utils/helpers";
import { JSCollection } from "entities/JSCollection";
import { executeJSFunction } from "actions/jsPaneActions";
import {
executeAppAction,
TriggerMeta,
@ -587,85 +590,109 @@ function* runActionSaga(
}
}
function* executePageLoadAction(pageAction: PageAction) {
const pageId = yield select(getCurrentPageId);
let currentApp: ApplicationPayload = yield select(getCurrentApplication);
currentApp = currentApp || {};
const appMode = yield select(getAppMode);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: pageAction.pluginType,
name: pageAction.name,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
isExampleApp: currentApp.appIsExample,
});
let payload = EMPTY_RESPONSE;
let isError = true;
const error = `The action "${pageAction.name}" has failed.`;
try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
pageAction,
function* executeOnPageLoadJSAction(pageAction: PageAction) {
const collectionId = pageAction.collectionId;
if (collectionId) {
const collection: JSCollection = yield select(
getJSCollection,
collectionId,
);
payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError;
} catch (e) {
log.error(e);
const jsAction = collection.actions.find((d) => d.id === pageAction.id);
if (!!jsAction) {
yield put(
executeJSFunction({
collectionName: collection.name,
action: jsAction,
collectionId: collectionId,
}),
);
}
}
}
if (isError) {
AppsmithConsole.addError({
id: pageAction.id,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed with status ${payload.statusCode}`,
source: {
type: ENTITY_TYPE.ACTION,
name: pageAction.name,
id: pageAction.id,
},
state: payload.request,
messages: [
{
message: error,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
},
],
function* executePageLoadAction(pageAction: PageAction) {
if (pageAction.hasOwnProperty("collectionId")) {
yield call(executeOnPageLoadJSAction, pageAction);
} else {
const pageId = yield select(getCurrentPageId);
let currentApp: ApplicationPayload = yield select(getCurrentApplication);
currentApp = currentApp || {};
const appMode = yield select(getAppMode);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: pageAction.pluginType,
name: pageAction.name,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
isExampleApp: currentApp.appIsExample,
});
yield put(
executePluginActionError({
actionId: pageAction.id,
isPageLoad: true,
error: { message: error },
data: payload,
}),
);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
failed: true,
},
pageAction.id,
);
} else {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
undefined,
pageAction.id,
);
yield put(
executePluginActionSuccess({
let payload = EMPTY_RESPONSE;
let isError = true;
const error = `The action "${pageAction.name}" has failed.`;
try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
pageAction,
);
payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError;
} catch (e) {
log.error(e);
}
if (isError) {
AppsmithConsole.addError({
id: pageAction.id,
response: payload,
isPageLoad: true,
}),
);
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed with status ${payload.statusCode}`,
source: {
type: ENTITY_TYPE.ACTION,
name: pageAction.name,
id: pageAction.id,
},
state: payload.request,
messages: [
{
message: error,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
},
],
});
yield put(
executePluginActionError({
actionId: pageAction.id,
isPageLoad: true,
error: { message: error },
data: payload,
}),
);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
failed: true,
},
pageAction.id,
);
} else {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
undefined,
pageAction.id,
);
yield put(
executePluginActionSuccess({
id: pageAction.id,
response: payload,
isPageLoad: true,
}),
);
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
}
}
}

View File

@ -220,9 +220,14 @@ export function* evaluateAndExecuteDynamicTrigger(
* We raise an error telling the user that an uncaught error has occurred
* */
if (requestData.result.errors.length) {
throw new UncaughtPromiseError(
requestData.result.errors[0].errorMessage,
);
if (
requestData.result.errors[0].errorMessage !==
"UncaughtPromiseRejection: User cancelled action execution"
) {
throw new UncaughtPromiseError(
requestData.result.errors[0].errorMessage,
);
}
}
// It is possible to get a few triggers here if the user
// still uses the old way of action runs and not promises. For that we

View File

@ -6,6 +6,7 @@ import {
debounce,
call,
take,
takeLatest,
} from "redux-saga/effects";
import {
ReduxAction,
@ -31,11 +32,16 @@ import {
pushLogsForObjectUpdate,
createDummyJSCollectionActions,
} from "../utils/JSPaneUtils";
import JSActionAPI, { RefactorAction } from "../api/JSActionAPI";
import JSActionAPI, {
RefactorAction,
SetFunctionPropertyPayload,
} from "../api/JSActionAPI";
import ActionAPI from "api/ActionAPI";
import {
updateJSCollectionSuccess,
refactorJSCollectionAction,
updateJSCollectionBodySuccess,
updateJSFunction,
} from "actions/jsPaneActions";
import { getCurrentOrgId } from "selectors/organizationSelectors";
import { getPluginIdOfPackageName } from "sagas/selectors";
@ -59,6 +65,7 @@ import LOG_TYPE from "entities/AppsmithConsole/logtype";
import PageApi from "api/PageApi";
import { updateCanvasWithDSL } from "sagas/PageSagas";
export const JS_PLUGIN_PACKAGE_NAME = "js-plugin";
import { set } from "lodash";
import { updateReplayEntity } from "actions/pageActions";
function* handleCreateNewJsActionSaga(action: ReduxAction<{ pageId: string }>) {
@ -280,7 +287,7 @@ function* handleJSObjectNameChangeSuccessSaga(
}
}
function* handleExecuteJSFunctionSaga(
export function* handleExecuteJSFunctionSaga(
data: ReduxAction<{
collectionName: string;
action: JSAction;
@ -291,7 +298,6 @@ function* handleExecuteJSFunctionSaga(
const actionId = action.id;
try {
const result = yield call(executeFunction, collectionName, action);
yield put({
type: ReduxActionTypes.EXECUTE_JS_FUNCTION_SUCCESS,
payload: {
@ -338,7 +344,7 @@ function* handleUpdateJSCollectionBody(
actionPayload: ReduxAction<{ body: string; id: string; isReplay: boolean }>,
) {
const jsCollection = yield select(getJSCollection, actionPayload.payload.id);
jsCollection.body = actionPayload.payload.body;
jsCollection["body"] = actionPayload.payload.body;
try {
if (jsCollection) {
const response = yield JSActionAPI.updateJSCollection(jsCollection);
@ -418,6 +424,132 @@ function* handleRefactorJSActionNameSaga(
}
}
function* setFunctionPropertySaga(
data: ReduxAction<SetFunctionPropertyPayload>,
) {
const { action, propertyName, value } = data.payload;
if (!action.id) return;
const actionId = action.id;
if (propertyName === "executeOnLoad") {
yield put({
type: ReduxActionTypes.TOGGLE_FUNCTION_EXECUTE_ON_LOAD_INIT,
payload: {
collectionId: action.collectionId,
actionId,
shouldExecute: value,
},
});
return;
}
yield put(updateJSFunction({ ...data.payload }));
}
function* handleUpdateJSFunctionPropertySaga(
data: ReduxAction<SetFunctionPropertyPayload>,
) {
const { action, propertyName, value } = data.payload;
if (!action.id) return;
const actionId = action.id;
let collection: JSCollection;
if (action.collectionId) {
collection = yield select(getJSCollection, action.collectionId);
try {
const actions: JSAction[] = collection.actions;
const updatedActions = actions.map((jsAction: JSAction) => {
if (jsAction.id === actionId) {
set(jsAction, propertyName, value);
return jsAction;
}
return jsAction;
});
collection.actions = updatedActions;
const response = yield JSActionAPI.updateJSCollection(collection);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
const fieldToBeUpdated = propertyName.replace(
"actionConfiguration",
"config",
);
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_UPDATE,
text: "Configuration updated",
source: {
type: ENTITY_TYPE.JSACTION,
name: collection.name + "." + action.name,
id: actionId,
propertyPath: fieldToBeUpdated,
},
state: {
[fieldToBeUpdated]: value,
},
});
yield put({
type: ReduxActionTypes.UPDATE_JS_FUNCTION_PROPERTY_SUCCESS,
payload: {
collection,
},
});
}
} catch (e) {
yield put({
type: ReduxActionErrorTypes.UPDATE_JS_FUNCTION_PROPERTY_ERROR,
payload: collection,
});
}
}
}
function* toggleFunctionExecuteOnLoadSaga(
action: ReduxAction<{
collectionId: string;
actionId: string;
shouldExecute: boolean;
}>,
) {
try {
const { actionId, collectionId, shouldExecute } = action.payload;
const collection = yield select(getJSCollection, collectionId);
const jsAction = collection.actions.find(
(action: JSAction) => actionId === action.id,
);
const response = yield call(
ActionAPI.toggleActionExecuteOnLoad,
actionId,
shouldExecute,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_UPDATE,
text: "Configuration updated",
source: {
type: ENTITY_TYPE.JSACTION,
name: collection.name + "." + jsAction.name,
id: actionId,
propertyPath: "executeOnLoad",
},
state: {
["executeOnLoad"]: shouldExecute,
},
});
yield put({
type: ReduxActionTypes.TOGGLE_FUNCTION_EXECUTE_ON_LOAD_SUCCESS,
payload: {
actionId: actionId,
collectionId: collectionId,
executeOnLoad: shouldExecute,
},
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.TOGGLE_ACTION_EXECUTE_ON_LOAD_ERROR,
payload: error,
});
}
}
export default function* root() {
yield all([
takeEvery(
@ -445,5 +577,14 @@ export default function* root() {
ReduxActionTypes.UPDATE_JS_ACTION_BODY_INIT,
handleUpdateJSCollectionBody,
),
takeEvery(ReduxActionTypes.SET_FUNCTION_PROPERTY, setFunctionPropertySaga),
takeLatest(
ReduxActionTypes.UPDATE_JS_FUNCTION_PROPERTY_INIT,
handleUpdateJSFunctionPropertySaga,
),
takeLatest(
ReduxActionTypes.TOGGLE_FUNCTION_EXECUTE_ON_LOAD_INIT,
toggleFunctionExecuteOnLoadSaga,
),
]);
}

View File

@ -75,6 +75,7 @@ import {
executePageLoadActions,
fetchActionsForPage,
setActionsToExecuteOnPageLoad,
setJSActionsToExecuteOnPageLoad,
} from "actions/pluginActionActions";
import { UrlDataState } from "reducers/entityReducers/appReducer";
import { APP_MODE } from "entities/App";
@ -403,7 +404,18 @@ function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) {
}
// Update actions
if (actionUpdates && actionUpdates.length > 0) {
yield put(setActionsToExecuteOnPageLoad(actionUpdates));
const actions = actionUpdates.filter(
(d) => !d.hasOwnProperty("collectionId"),
);
if (actions && actions.length) {
yield put(setActionsToExecuteOnPageLoad(actions));
}
const jsActions = actionUpdates.filter((d) =>
d.hasOwnProperty("collectionId"),
);
if (jsActions && jsActions.length) {
yield put(setJSActionsToExecuteOnPageLoad(jsActions));
}
}
yield put(setLastUpdatedTime(Date.now() / 1000));
yield put(savePageSuccess(savePageResponse));

View File

@ -307,10 +307,13 @@ export const unsafeFunctionForEval = [
export const isChildPropertyPath = (
parentPropertyPath: string,
childPropertyPath: string,
): boolean =>
parentPropertyPath === childPropertyPath ||
childPropertyPath.startsWith(`${parentPropertyPath}.`) ||
childPropertyPath.startsWith(`${parentPropertyPath}[`);
): boolean => {
return (
parentPropertyPath === childPropertyPath ||
childPropertyPath.startsWith(`${parentPropertyPath}.`) ||
childPropertyPath.startsWith(`${parentPropertyPath}[`)
);
};
/**
* Paths set via evaluator on entities

View File

@ -113,7 +113,7 @@ export const getDifferenceInJSCollection = (
actionConfiguration: {
body: action.body,
isAsync: action.isAsync,
timeoutInMilliseconds: 0,
timeoutInMillisecond: 0,
jsArguments: [],
},
};
@ -196,9 +196,10 @@ export const createDummyJSCollectionActions = (
actionConfiguration: {
body: "() => {\n\t\t//write code here\n\t}",
isAsync: false,
timeoutInMilliseconds: 0,
timeoutInMillisecond: 0,
jsArguments: [],
},
clientSideExecution: true,
},
{
name: "myFun2",
@ -206,11 +207,12 @@ export const createDummyJSCollectionActions = (
organizationId,
executeOnLoad: false,
actionConfiguration: {
body: "async () => {\n\t\t//write code here\n\t}",
body: "async () => {\n\t\t//use async-await or promises\n\t}",
isAsync: true,
timeoutInMilliseconds: 0,
timeoutInMillisecond: 0,
jsArguments: [],
},
clientSideExecution: true,
},
];
return {

View File

@ -178,13 +178,27 @@ export default class DataTreeEvaluator {
return { evalTree: this.evalTree, jsUpdates: jsUpdates };
}
isJSObjectFunction(dataTree: DataTree, jsObjectName: string, key: string) {
const entity = dataTree[jsObjectName];
if (isJSAction(entity)) {
return entity.meta.hasOwnProperty(key);
}
return false;
}
updateLocalUnEvalTree(dataTree: DataTree) {
//add functions and variables to unevalTree
Object.keys(this.currentJSCollectionState).forEach((update) => {
const updates = this.currentJSCollectionState[update];
if (!!dataTree[update]) {
Object.keys(updates).forEach((key) => {
_.set(dataTree, `${update}.${key}`, updates[key]);
const data = _.get(dataTree, `${update}.${key}.data`, undefined);
if (this.isJSObjectFunction(dataTree, update, key)) {
_.set(dataTree, `${update}.${key}`, new String(updates[key]));
_.set(dataTree, `${update}.${key}.data`, data);
} else {
_.set(dataTree, `${update}.${key}`, updates[key]);
}
});
}
});
@ -548,7 +562,8 @@ export default class DataTreeEvaluator {
Object.keys(entity.bindingPaths).forEach((propertyPath) => {
const existingDeps =
dependencies[`${entityName}.${propertyPath}`] || [];
const jsSnippets = [_.get(entity, propertyPath)];
const unevalPropValue = _.get(entity, propertyPath).toString();
const { jsSnippets } = getDynamicBindings(unevalPropValue, entity);
dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat(
jsSnippets.filter((jsSnippet) => !!jsSnippet),
);

View File

@ -9,7 +9,10 @@ const ctx: Worker = self as any;
* needs a REQUEST_ID to be passed in to know which request is going on right now
*/
import { EVAL_WORKER_ACTIONS } from "utils/DynamicBindingUtils";
import { ActionDescription } from "entities/DataTree/actionTriggers";
import {
ActionDescription,
ActionTriggerType,
} from "entities/DataTree/actionTriggers";
import _ from "lodash";
import { dataTreeEvaluator } from "workers/evaluation.worker";
@ -99,3 +102,18 @@ export const completePromise = (requestId: string, result: EvalResult) => {
requestId,
});
};
export const confirmationPromise = function(
requestId: string,
func: any,
name: string,
...args: any[]
) {
const payload: ActionDescription = {
type: ActionTriggerType.CONFIRMATION_MODAL,
payload: {
funName: name,
},
};
return promisifyAction(requestId, payload).then(() => func(...args));
};

View File

@ -11,7 +11,7 @@ import { Severity } from "entities/AppsmithConsole";
import { enhanceDataTreeWithFunctions } from "./Actions";
import { isEmpty } from "lodash";
import { getLintingErrors } from "workers/lint";
import { completePromise } from "workers/PromisifyAction";
import { completePromise, confirmationPromise } from "workers/PromisifyAction";
import { ActionDescription } from "entities/DataTree/actionTriggers";
export type EvalResult = {
@ -147,7 +147,23 @@ export const createGlobalData = (
Object.keys(resolvedObject).forEach((key: any) => {
const dataTreeKey = GLOBAL_DATA[datum];
if (dataTreeKey) {
dataTreeKey[key] = resolvedObject[key];
const data = dataTreeKey[key]?.data;
const isAsync = dataTreeKey?.meta[key]?.isAsync || false;
const confirmBeforeExecute =
dataTreeKey?.meta[key]?.confirmBeforeExecute || false;
if (isAsync && confirmBeforeExecute) {
dataTreeKey[key] = confirmationPromise.bind(
{},
context?.requestId,
resolvedObject[key],
dataTreeKey.name + "." + key,
);
} else {
dataTreeKey[key] = resolvedObject[key];
}
if (!!data) {
dataTreeKey[key]["data"] = data;
}
}
});
});
@ -355,7 +371,23 @@ export function isFunctionAsync(
Object.keys(resolvedObject).forEach((key: any) => {
const dataTreeKey = GLOBAL_DATA[datum];
if (dataTreeKey) {
dataTreeKey[key] = resolvedObject[key];
const data = dataTreeKey[key]?.data;
const isAsync = dataTreeKey.meta[key]?.isAsync || false;
const confirmBeforeExecute =
dataTreeKey.meta[key]?.confirmBeforeExecute || false;
if (isAsync && confirmBeforeExecute) {
dataTreeKey[key] = confirmationPromise.bind(
{},
"",
resolvedObject[key],
key,
);
} else {
dataTreeKey[key] = resolvedObject[key];
}
if (!!data) {
dataTreeKey[key].data = data;
}
}
});
});
@ -368,7 +400,6 @@ export function isFunctionAsync(
// @ts-ignore: No types available
self[key] = GLOBAL_DATA[key];
});
try {
if (typeof userFunction === "function") {
const returnValue = userFunction();

View File

@ -605,33 +605,68 @@ export const updateJSCollectionInDataTree = (
const action = parsedBody.actions[i];
if (jsCollection.hasOwnProperty(action.name)) {
if (jsCollection[action.name] !== action.body) {
const data = _.get(
modifiedDataTree,
`${jsCollection.name}.${action.name}.data`,
{},
);
_.set(
modifiedDataTree,
`${jsCollection.name}.${action.name}`,
action.body,
new String(action.body),
);
_.set(
modifiedDataTree,
`${jsCollection.name}.${action.name}.data`,
data,
);
}
} else {
const bindingPaths = jsCollection.bindingPaths;
bindingPaths[action.name] = EvaluationSubstitutionType.SMART_SUBSTITUTE;
_.set(modifiedDataTree, `${jsCollection}.bindingPaths`, bindingPaths);
bindingPaths[`${action.name}.data`] =
EvaluationSubstitutionType.TEMPLATE;
_.set(
modifiedDataTree,
`${jsCollection.name}.bindingPaths`,
bindingPaths,
);
const dynamicBindingPathList = jsCollection.dynamicBindingPathList;
dynamicBindingPathList.push({ key: action.name });
_.set(
modifiedDataTree,
`${jsCollection}.dynamicBindingPathList`,
`${jsCollection.name}.dynamicBindingPathList`,
dynamicBindingPathList,
);
const dependencyMap = jsCollection.dependencyMap;
dependencyMap["body"].push(action.name);
_.set(modifiedDataTree, `${jsCollection}.dependencyMap`, dependencyMap);
_.set(
modifiedDataTree,
`${jsCollection.name}.dependencyMap`,
dependencyMap,
);
const meta = jsCollection.meta;
meta[action.name] = { arguments: action.arguments };
meta[action.name] = {
arguments: action.arguments,
isAsync: false,
confirmBeforeExecute: false,
};
_.set(modifiedDataTree, `${jsCollection.name}.meta`, meta);
const data = _.get(
modifiedDataTree,
`${jsCollection.name}.${action.name}.data`,
{},
);
_.set(
modifiedDataTree,
`${jsCollection.name}.${action.name}`,
action.body,
new String(action.body.toString()),
);
_.set(
modifiedDataTree,
`${jsCollection.name}.${action.name}.data`,
data,
);
}
}
@ -675,6 +710,7 @@ export const updateJSCollectionInDataTree = (
delete meta[preAction];
_.set(modifiedDataTree, `${jsCollection.name}.meta`, meta);
delete modifiedDataTree[`${jsCollection.name}`][`${preAction}`];
delete modifiedDataTree[`${jsCollection.name}`][`${preAction}.data`];
}
}
}