From 6580c6033c7853faa474a0bfac5b0f2277aea52c Mon Sep 17 00:00:00 2001 From: Rishabh Rathod Date: Thu, 7 Dec 2023 00:48:52 +0530 Subject: [PATCH] chore: Action execution changes for modules (#29357) --- app/client/src/actions/jsPaneActions.ts | 6 +- .../src/ce/selectors/entitiesSelector.ts | 20 ++- .../src/ce/utils/actionExecutionUtils.ts | 17 +++ .../src/ee/utils/actionExecutionUtils.ts | 1 + app/client/src/pages/Editor/JSEditor/Form.tsx | 3 +- .../entityReducers/jsActionsReducer.tsx | 5 +- .../sagas/ActionExecution/PluginActionSaga.ts | 129 +++++++++--------- app/client/src/sagas/EvaluationsSaga.ts | 25 +--- app/client/src/sagas/JSPaneSagas.ts | 79 +++++++---- app/client/src/sagas/PostEvaluationSagas.ts | 7 +- 10 files changed, 168 insertions(+), 124 deletions(-) create mode 100644 app/client/src/ce/utils/actionExecutionUtils.ts create mode 100644 app/client/src/ee/utils/actionExecutionUtils.ts diff --git a/app/client/src/actions/jsPaneActions.ts b/app/client/src/actions/jsPaneActions.ts index a5aa62e7dd4..51319844334 100644 --- a/app/client/src/actions/jsPaneActions.ts +++ b/app/client/src/actions/jsPaneActions.ts @@ -60,9 +60,8 @@ export const refactorJSCollectionAction = (payload: { }; export const executeJSFunctionInit = (payload: { - collectionName: string; + collection: JSCollection; action: JSAction; - collectionId: string; }) => { return { type: ReduxActionTypes.EXECUTE_JS_FUNCTION_INIT, @@ -71,9 +70,8 @@ export const executeJSFunctionInit = (payload: { }; export const startExecutingJSFunction = (payload: { - collectionName: string; action: JSAction; - collectionId: string; + collection: JSCollection; from: EventLocation; }) => { return { diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index 1689d3fe291..61a6087be02 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -28,7 +28,7 @@ import type { DefaultPlugin, GenerateCRUDEnabledPluginMap, } from "api/PluginApi"; -import type { JSAction, JSCollection } from "entities/JSCollection"; +import type { JSAction } from "entities/JSCollection"; import { APP_MODE } from "entities/App"; import type { ExplorerFileEntity } from "@appsmith/pages/Editor/Explorer/helpers"; import type { ActionValidationConfigMap } from "constants/PropertyControlConstants"; @@ -673,15 +673,27 @@ export const getActionData = ( return action ? action.data : undefined; }; -export const getJSCollection = ( +export const getJSCollection = (state: AppState, actionId: string) => { + const jsaction = find( + state.entities.jsActions, + (a) => a.config.id === actionId, + ); + return jsaction && jsaction.config; +}; + +/** + * + * getJSCollectionFromAllEntities is used to get the js collection from all jsAction entities (including module instance entities) ) + */ +export const getJSCollectionFromAllEntities = ( state: AppState, actionId: string, -): JSCollection | undefined => { +) => { const jsaction = find( state.entities.jsActions, (a) => a.config.id === actionId, ); - return jsaction ? jsaction.config : undefined; + return jsaction && jsaction.config; }; export function getCurrentPageNameByActionId( diff --git a/app/client/src/ce/utils/actionExecutionUtils.ts b/app/client/src/ce/utils/actionExecutionUtils.ts new file mode 100644 index 00000000000..935c4514759 --- /dev/null +++ b/app/client/src/ce/utils/actionExecutionUtils.ts @@ -0,0 +1,17 @@ +import type { Action } from "entities/Action"; +import type { JSAction, JSCollection } from "entities/JSCollection"; + +export function getPluginActionNameToDisplay(action: Action) { + return action.name; +} + +export function getJSActionPathNameToDisplay( + action: JSAction, + collection: JSCollection, +) { + return collection.name + "." + action.name; +} + +export function getJSActionNameToDisplay(action: JSAction) { + return action.name; +} diff --git a/app/client/src/ee/utils/actionExecutionUtils.ts b/app/client/src/ee/utils/actionExecutionUtils.ts new file mode 100644 index 00000000000..08da5b42faf --- /dev/null +++ b/app/client/src/ee/utils/actionExecutionUtils.ts @@ -0,0 +1 @@ +export * from "ce/utils/actionExecutionUtils"; diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index 1cf553cf7e6..0ec7c4cae23 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -207,9 +207,8 @@ function JSEditorForm({ ); dispatch( startExecutingJSFunction({ - collectionName: currentJSCollection.name || "", action: jsAction, - collectionId: currentJSCollection.id || "", + collection: currentJSCollection, from: from, }), ); diff --git a/app/client/src/reducers/entityReducers/jsActionsReducer.tsx b/app/client/src/reducers/entityReducers/jsActionsReducer.tsx index 68dcc361caf..b7d722a1bee 100644 --- a/app/client/src/reducers/entityReducers/jsActionsReducer.tsx +++ b/app/client/src/reducers/entityReducers/jsActionsReducer.tsx @@ -290,13 +290,12 @@ const jsActionsReducer = createReducer(initialState, { [ReduxActionTypes.EXECUTE_JS_FUNCTION_INIT]: ( state: JSCollectionDataState, action: ReduxAction<{ - collectionName: string; - collectionId: string; + collection: JSCollection; action: JSAction; }>, ): JSCollectionDataState => state.map((a) => { - if (a.config.id === action.payload.collectionId) { + if (a.config.id === action.payload.collection.id) { const newData = { ...a.data }; const newIsDirty = { ...a.isDirty }; unset(newData, action.payload.action.id); diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index d9ada911bbe..1b60ebfdfb0 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -42,8 +42,8 @@ import { getPlugin, isActionDirty, isActionSaving, - getJSCollection, getDatasource, + getJSCollectionFromAllEntities, } from "@appsmith/selectors/entitiesSelector"; import { getIsGitSyncModalOpen } from "selectors/gitSyncSelectors"; import { @@ -130,7 +130,6 @@ import { UserCancelledActionExecutionError, } from "sagas/ActionExecution/errorUtils"; import { shouldBeDefined, trimQueryString } from "utils/helpers"; -import type { JSCollection } from "entities/JSCollection"; import { requestModalConfirmationSaga } from "sagas/UtilSagas"; import { ModalType } from "reducers/uiReducers/modalActionReducer"; import { getFormNames, getFormValues } from "redux-form"; @@ -169,6 +168,10 @@ import { setAttributesToSpan, startRootSpan, } from "UITelemetry/generateTraces"; +import { + getJSActionPathNameToDisplay, + getPluginActionNameToDisplay, +} from "@appsmith/utils/actionExecutionUtils"; enum ActionResponseDataTypes { BINARY = "BINARY", @@ -1023,71 +1026,74 @@ function* executeOnPageLoadJSAction(pageAction: PageAction) { const collectionId = pageAction.collectionId; const pageId: string | undefined = yield select(getCurrentPageId); - if (collectionId) { - const collection: JSCollection = yield select( - getJSCollection, - collectionId, - ); + if (!collectionId) return; - if (!collection) { - Sentry.captureException( - new Error( - "Collection present in layoutOnLoadActions but no collection exists ", - ), - { - extra: { - collectionId, - actionId: pageAction.id, - pageId, - }, + const collection: ReturnType = + yield select(getJSCollectionFromAllEntities, collectionId); + + if (!collection) { + Sentry.captureException( + new Error( + "Collection present in layoutOnLoadActions but no collection exists ", + ), + { + extra: { + collectionId, + actionId: pageAction.id, + pageId, }, - ); + }, + ); + return; + } - return; - } + const jsAction = collection.actions.find( + (action) => action.id === pageAction.id, + ); + if (!!jsAction) { + if (jsAction.confirmBeforeExecute) { + const jsActionPathNameToDisplay = getJSActionPathNameToDisplay( + jsAction, + collection, + ); + const modalPayload = { + name: jsActionPathNameToDisplay, + modalOpen: true, + modalType: ModalType.RUN_ACTION, + }; - const jsAction = collection.actions.find( - (action) => action.id === pageAction.id, - ); - if (!!jsAction) { - if (jsAction.confirmBeforeExecute) { - const modalPayload = { - name: pageAction.name, - modalOpen: true, - modalType: ModalType.RUN_ACTION, - }; + const confirmed: boolean = yield call( + requestModalConfirmationSaga, + modalPayload, + ); + if (!confirmed) { + yield put({ + type: ReduxActionTypes.RUN_ACTION_CANCELLED, + payload: { id: pageAction.id }, + }); + + const jsActionPathNameToDisplay = getJSActionPathNameToDisplay( + jsAction, + collection, + ); - const confirmed: unknown = yield call( - requestModalConfirmationSaga, - modalPayload, + toast.show( + createMessage(ACTION_EXECUTION_CANCELLED, jsActionPathNameToDisplay), + { + kind: "error", + }, ); - if (!confirmed) { - yield put({ - type: ReduxActionTypes.RUN_ACTION_CANCELLED, - payload: { id: pageAction.id }, - }); - toast.show( - createMessage( - ACTION_EXECUTION_CANCELLED, - `${collection.name}.${jsAction.name}`, - ), - { - kind: "error", - }, - ); - // Don't proceed to executing the js function - return; - } + // Don't proceed to executing the js function + return; } - const data = { - collectionName: collection.name, - action: jsAction, - collectionId: collectionId, - isExecuteJSFunc: true, - }; - - yield call(handleExecuteJSFunctionSaga, data); } + const data = { + action: jsAction, + collection, + isExecuteJSFunc: true, + }; + + yield call(handleExecuteJSFunctionSaga, data); } } @@ -1331,14 +1337,15 @@ function* executePluginActionSaga( parentSpan?: OtlpSpan, ) { const actionId = pluginAction.id; + const pluginActionNameToDisplay = getPluginActionNameToDisplay(pluginAction); parentSpan && setAttributesToSpan(parentSpan, { actionId, - pluginName: pluginAction?.name, + pluginName: pluginActionNameToDisplay, }); if (pluginAction.confirmBeforeExecute) { const modalPayload = { - name: pluginAction.name, + name: pluginActionNameToDisplay, modalOpen: true, modalType: ModalType.RUN_ACTION, }; diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 4f81763e9a8..50284f1fc24 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -58,7 +58,7 @@ import { postEvalActionDispatcher, updateTernDefinitions, } from "./PostEvaluationSagas"; -import type { JSAction } from "entities/JSCollection"; +import type { JSAction, JSCollection } from "entities/JSCollection"; import { getAppMode } from "@appsmith/selectors/applicationSelectors"; import { APP_MODE } from "entities/App"; import { get, isEmpty } from "lodash"; @@ -397,11 +397,8 @@ interface JSFunctionExecutionResponse { logs?: LogObject[]; } -function* executeAsyncJSFunction( - collectionName: string, - action: JSAction, - collectionId: string, -) { +function* executeAsyncJSFunction(action: JSAction, collection: JSCollection) { + const { id: collectionId, name: collectionName } = collection; const functionCall = `${collectionName}.${action.name}()`; const triggerMeta = { source: { @@ -422,27 +419,17 @@ function* executeAsyncJSFunction( return response; } -export function* executeJSFunction( - collectionName: string, - action: JSAction, - collectionId: string, -) { +export function* executeJSFunction(action: JSAction, collection: JSCollection) { const response: { errors: unknown[]; result: unknown; logs?: LogObject[]; - } = yield call(executeAsyncJSFunction, collectionName, action, collectionId); + } = yield call(executeAsyncJSFunction, action, collection); const { errors, result } = response; const isDirty = !!errors.length; // After every function execution, log execution errors if present - yield call( - handleJSFunctionExecutionErrorLog, - collectionId, - collectionName, - action, - errors, - ); + yield call(handleJSFunctionExecutionErrorLog, action, collection, errors); return { result, isDirty }; } diff --git a/app/client/src/sagas/JSPaneSagas.ts b/app/client/src/sagas/JSPaneSagas.ts index 33b4a45e359..b7230498d41 100644 --- a/app/client/src/sagas/JSPaneSagas.ts +++ b/app/client/src/sagas/JSPaneSagas.ts @@ -91,6 +91,10 @@ import { setDebuggerSelectedTab, showDebugger } from "actions/debuggerActions"; import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; import { getDebuggerSelectedTab } from "selectors/debuggerSelectors"; import { getIsServerDSLMigrationsEnabled } from "selectors/pageSelectors"; +import { + getJSActionNameToDisplay, + getJSActionPathNameToDisplay, +} from "@appsmith/utils/actionExecutionUtils"; const CONSOLE_DOT_LOG_INVOCATION_REGEX = /console.log[.call | .apply]*\s*\(.*?\)/gm; @@ -358,22 +362,21 @@ function* handleJSObjectNameChangeSuccessSaga( //isExecuteJSFunc is used to check if the function is called on the JS Function execution. export function* handleExecuteJSFunctionSaga(data: { - collectionName: string; action: JSAction; - collectionId: string; + collection: JSCollection; isExecuteJSFunc: boolean; -}): any { - const { action, collectionId, collectionName, isExecuteJSFunc } = data; +}) { + const { action, collection, isExecuteJSFunc } = data; + const { id: collectionId } = collection; const actionId = action.id; const appMode: APP_MODE = yield select(getAppMode); yield put( executeJSFunctionInit({ - collectionName, + collection, action, - collectionId, }), ); - const isEntitySaving = yield select(getIsSavingEntity); + const isEntitySaving: boolean = yield select(getIsSavingEntity); /** * Only start executing when no entity in the application is saving * This ensures that execution doesn't get carried out on stale values @@ -383,18 +386,27 @@ export function* handleExecuteJSFunctionSaga(data: { yield take(ReduxActionTypes.ENTITY_UPDATE_SUCCESS); } + const doesURLPathContainCollectionId = + window.location.pathname.includes(collectionId); + + const jsActionPathNameToDisplay = getJSActionPathNameToDisplay( + action, + collection, + ); + try { const { isDirty, result } = yield call( executeJSFunction, - collectionName, action, - collectionId, + collection, ); // open response tab in debugger on runnning or page load js action. - if (window.location.pathname.includes(collectionId)) { + + if (doesURLPathContainCollectionId) { yield put(showDebugger(true)); - const debuggerSelectedTab = yield select(getDebuggerSelectedTab); + const debuggerSelectedTab: ReturnType = + yield select(getDebuggerSelectedTab); yield put( setDebuggerSelectedTab( @@ -410,25 +422,34 @@ export function* handleExecuteJSFunctionSaga(data: { isDirty, }, }); + + const jsActionNameToDisplay = getJSActionNameToDisplay(action); AppsmithConsole.info({ text: createMessage(JS_EXECUTION_SUCCESS), source: { type: ENTITY_TYPE.JSACTION, - name: collectionName + "." + action.name, + name: jsActionPathNameToDisplay, id: collectionId, }, state: { response: result }, }); const showSuccessToast = appMode === APP_MODE.EDIT && !isDirty; - showSuccessToast && + + if ( + showSuccessToast && isExecuteJSFunc && - !window.location.pathname.includes(collectionId) && - toast.show(createMessage(JS_EXECUTION_SUCCESS_TOASTER, action.name), { - kind: "success", - }); + !doesURLPathContainCollectionId + ) { + toast.show( + createMessage(JS_EXECUTION_SUCCESS_TOASTER, jsActionNameToDisplay), + { + kind: "success", + }, + ); + } } catch (error) { // open response tab in debugger on runnning js action. - if (window.location.pathname.includes(collectionId)) { + if (doesURLPathContainCollectionId) { yield put(showDebugger(true)); yield put(setDebuggerSelectedTab(DEBUGGER_TAB_KEYS.RESPONSE_TAB)); } @@ -440,7 +461,7 @@ export function* handleExecuteJSFunctionSaga(data: { text: createMessage(JS_EXECUTION_FAILURE), source: { type: ENTITY_TYPE.JSACTION, - name: collectionName + "." + action.name, + name: jsActionPathNameToDisplay, id: collectionId, }, messages: [ @@ -460,22 +481,25 @@ export function* handleExecuteJSFunctionSaga(data: { export function* handleStartExecuteJSFunctionSaga( data: ReduxAction<{ - collectionName: string; action: JSAction; - collectionId: string; + collection: JSCollection; from: EventLocation; }>, -): any { - const { action, collectionId, collectionName, from } = data.payload; +) { + const { action, collection, from } = data.payload; const actionId = action.id; + const JSActionPathName = getJSActionPathNameToDisplay(action, collection); if (action.confirmBeforeExecute) { const modalPayload = { - name: collectionName + "." + action.name, + name: JSActionPathName, modalOpen: true, modalType: ModalType.RUN_ACTION, }; - const confirmed = yield call(requestModalConfirmationSaga, modalPayload); + const confirmed: boolean = yield call( + requestModalConfirmationSaga, + modalPayload, + ); if (!confirmed) { yield put({ @@ -496,9 +520,8 @@ export function* handleStartExecuteJSFunctionSaga( }); yield call(handleExecuteJSFunctionSaga, { - collectionName: collectionName, - action: action, - collectionId: collectionId, + action, + collection, isExecuteJSFunc: false, }); } diff --git a/app/client/src/sagas/PostEvaluationSagas.ts b/app/client/src/sagas/PostEvaluationSagas.ts index cf97083eaae..383c17e8ddf 100644 --- a/app/client/src/sagas/PostEvaluationSagas.ts +++ b/app/client/src/sagas/PostEvaluationSagas.ts @@ -33,7 +33,7 @@ import { getAppMode } from "@appsmith/selectors/applicationSelectors"; import { APP_MODE } from "entities/App"; import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator"; import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService"; -import type { JSAction } from "entities/JSCollection"; +import type { JSAction, JSCollection } from "entities/JSCollection"; import { isWidgetPropertyNamePath } from "utils/widgetEvalUtils"; import type { ActionEntityConfig } from "@appsmith/entities/DataTree/types"; import type { SuccessfulBindings } from "utils/SuccessfulBindingsMap"; @@ -241,11 +241,12 @@ export function* updateTernDefinitions( } export function* handleJSFunctionExecutionErrorLog( - collectionId: string, - collectionName: string, action: JSAction, + collection: JSCollection, errors: any[], ) { + const { id: collectionId, name: collectionName } = collection; + errors.length ? AppsmithConsole.addErrors([ {