PromucFlow_constructor/app/client/src/sagas/EvaluationsSaga.ts
Abhinav Jha 4361db4269
feat: Automatic height updates for widgets based on contents (Auto Height) (#18341)
* added multi select back

* (WIP): Complete the dynamc height update logic

* (WIP): Dynamic height logic

* (WIP): Container computation logic, Next steps: Prevent reflow when resize is disabled. Fix logic of widgets randomly changing positions (Debug)

* Fix logic in container computations

* Integrate for PoC

* fixed the no initial load dynamic height updates

* Stop vertical resize and reflow when dynamic height is enabled for a widget

* added another container in text widget

* enabled dynamic height for container widgets

* removed dynamic height feature from list widget

* Fixed Button and Input components height increase

* added an experiment to overflow the content if maxHEight is less

* removed the ref of Textwidget by mistake, added it back

* fixed text widget height overflow problem with a little hack

* added long labels with text

* fixed the table scroll issue

* overflow fixed for json form widget

* added extra 8px height for Switch, Rating and Checkbox Height

* (WIP): Resolve issues

* (WIP): Fix widget padding issue

* added overflow container for Radio and Switch group widgets

* (WIP): Have modals work with dynamic height

* added the overlay and the handles

* added dragging behavior to the dots

* fixed the overlapping with the selection tool

* (WIP): Fix issues reported

* now we can update the property pane values back from overlay handles

* now we can update the property pane values back from overlay handles

* (WIP): Fix table widget

* Fix package.json

* Remove unit tests temporarily

* Fix unit test

* (WIP): Fix modal resize. Fix cursors. Fix border issue on non-resizable widgets

* fetch component heights using the requestAnimationFrame callback

* behavioural changes

* (WIP): Fix issues on the platform

* Update main container size appropriately

* more behavioural changes

* overlay now only be visible when hovering over the dots

* grid showing and widget reselecting

* added onfocus and onblur events to property pane listeners

* added onfocus and onblur events to property pane listeners

* added a range slider for min and max

* added demarcations for slider values

* (WIP): Fix platform workflows for dynamic height

* Fix issues with widgets

* Fix removed import

* - Add missing cypress files

* set the limits

* limit increase on change

* Fix z-index of min max limit indicators. Fix unused-vars warnings

* Fix Table Widget and Text Widget issues

* Fix: all the bugs in the bug master list for DH (#16268)

* changed the zindex for the signifiers

* showing signifiers only when the widget is selected

* made changes suggested by Momcilo

* activate the dots when the fields are active

* created a new centered dot handle

* removed overlays on focus and made the border more like deisgn

* handles on top of other widgets

* hide the overlay when multiple widgets are selected

* added a white border

* added a white border

* bug #15509 resolved

* changed the minDynamicHeightLimit to 2 instead of 4 to fix the Bug #15527

* removed the height auto fix from BaseInputComponent to fix the Bug #15388

* removed the condition to not ccalculate dynamic height when the row difference is less than 2 to fix the bug 15353

* made fixes for the bug #16307

* made fixes for the bug #16308

* made fixes for bug 16310

* made fixes for the bug #16402

* removed some log statements

* made fixes for the bug #16407

* fixed label problem found in the issue #16543

* made fixes for the issue #16547

* made fixes for the bug #16492

* redeploy

* (WIP): Fix to make this branch functional

* imported LabelWithTooltip back from design system

* signifier is now centered

* filled the signifier with primary color

* overlay hidden while dragging

* made the signifier dashed border also draggable

* Fix issue #16590 (#16798)

* set the limits to 4 rows

* replaced the static 40 value

* added signifiers for modal widget

* added signifiers for modal widget

* tried solving the scroll issue for widgets when there are limits

* solved the height problem using ResizeObserver

* (WIP): Fix maxDynamicHeight issue with container widgets:

* made the changes as per the review

* fixed the issue for input widget when label gets out of border

* hide text widget overflow options if auto height is enabled

* (WIP): In view mode, invisible widgets now donot take space (#16920)

* (WIP): In view mode, invisible widgets now donot take space

* (WIP): Enable the feature where invisible widgets in view mode don't take space to all widgets irrespective of the dynamic height feature

* Remove Replay conditional

* removed the scroll container for container type widgets

* removed the scroll container for container type widgets

* updated the hook to set overflow none for text widget

* fixed the should dynamic height logic to respect the min height limit

* Modal widget adheres to dynamic height (#16995)

* Modal widget adheres to dynamic height

* WIP: POC: fix dynamic height issues  (#16996)

Fix height less than 4 issue. Fix JSONForm adherence to min and max height

* POC: Dynamic height undo redo issue (#17085)

* Revert debouce timeout

* (WIP): Fix issue with undo-redo in dynamic height

* fix: Dynamic height issue fixes (#17153)

* Dynamic height issue fixes
==

- Fix issue where nested widgets did not ensure parent dynamic height updates
- Fix issue where Modal widget updates came in subsequent renders
- Fix issue where JSONForm collapses
- Fix performance issue for independent updates

* Use functions to get min and max dynamic height

* Fix issue where variable might have been undefined

* added the dynamic container into the deploy mode as well

* added overflow-x hidden when overflow-y is active in the dynamic height container

* fix: Dynamic height Issue fixes (#17204)

Fix preview mode invisible widgets. Fix Tabs widget dynamic height.

* removed a console.log statement

* removed the slider control file

* imported the LabelWithTooltip from the repo rather than ds

* word-break CSS rules added for Switch and Checkbox widget when Dynamic Height is enabled

* abstracted the check for dynamic height with limits enabled as isDynamicHeightWithLimitsEnabledForWidget

* abstracted the static value of 10 in dynamic height overlay to GridDefaults

* abstracted min and max dynamic height limits to getters

* fix: replaced all the refs for simpler widgets (#17353)

* replaced all the refs for simpler widgets

* removed the updateDynamicHeight from componentDidUpdate in BaseWidget

* added back lifecycle methods back to BaseWidget

* removed the contentRef from SwitchGroup and Table

* updating the height from the auto height with limits as well

* some hacks to make the limits work

* working solution

* used setTimeout to send an update to updateDynamicHeight from overlay update

* removed a log

* added requestanimationframe in settimeout

Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* Fix issues caused during merge

* Remove unneeded derived property

* removed more unnecessary code which should have been removed after removing the ref dependency

* fixed the maxDynamicHeight issue

* Fix issue where property configs were not being sent

* fix: Auto Height Feature - add selectors for tests (#17687)

Add selectors for auto height cypress tests

* fix: removed height auto default theme (#17415)

removed height auto css rule from the default theme

Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>

* fix: Auto Height Feature - Resolve issues and restructure code (#17686)

* Fix issues in dynamic height. Restructure code and reduce abstraction leaks

* Fix typescript issues

* Update based on review comments. Comment migrations, as a cyclic import is causing the jest tests to fail.

* Remove unused imports

* Decrease code nesting

* added the base styles for the overlay like position and z-index in its styled component css

* used the isDynamicHeightEnabled prop to set the height of SwitchGroup and RadioGroup widgets from 32px to 100% in case of inline mode

* fix: Auto Height - Resolve issues (#17737)

* Fix Tabs Widget showTabs toggle based auto height. Revert removal of BaseWidget code. Remove box-intersect and use a bruteforce algorithm. Add base logic for having containers collapse due to hidden child widgets

* Hide scroll contents and overflow property pane controls when dynamic height is enabled

* Removed the class property expectedHeight from BaseWidget as it is not useful in the overlay logic after some changes

* fixed the left alignment issue of label in the rich text editor by adding some styles applied only when the dynamic height is enabled

* fixed the input field stretching issue in case of Dynamic height by adding some CSS styles when isDynamicHeight is true

* Fix failing modal widget cypress tests

* Fix issue with scrollContents and Tabs Widget defaulTab

* added a little bit padding of 4px to the right of scroll container of dynamic height with limit

* Add test locators for resize handles

* removed the dynamic height logic from the table widget

* fix: Auto-Height invisible widgets (#17849)

* Fix issue where invisible widgets were still taking space

* Make sure to collapse only if dynamic height is enabled

* Fix issues with reflow (not the invisible widgets)

* Fix container min height issues

* Fix reflow with original bottom and top values. Testing needed

* Fix invisible widgets

* fix: enabled dynamic height for stat box widget (#17971)

enabled dynamic height for stat box widget

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: added a min height to rich text editor so that it does not collapse (#17970)

added a min height to rich text editor so that it does not collapse

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* Fix issue with resizing auto height widget

* Add helper text to educate users regarding the scroll disconnect in WYSIWYG

* fix: Auto Height Fixes (#18111)

AUTO HEIGHT FIXES

- Fix JSONForm height discrepancy
- Fix issue where widgets moved below the other
- Fix droptarget height after parent container resize

* fix: sliced up the DynamicHeightOverlay component a little bit (#18100)

* sliced up the DynamicHeightOverlay component a little bit

* more refactoring

* more refactoring

* used release event emitter and refactored more

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: rich text editor center alignment issue (#18142)

* removed the center alignment from rich text editor

* dummy commit

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: old DSL container collapse (#18160)

* Fix issue where old containers from old DSLs used to collapse when auto height was enabled

* Fix issue where old containers don't allow new widgets to be added when auto height is enabled, this is because the shouldScrollContents is undefined

* fix: input widgets issue (#18172)

fixed the auto height not working issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: preview deploy mode (#18174)

fixed the preview and deploy mode

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits label intersection with handle dot (#18186)

fixed the position of the limits label to the right so that it will not intersect with the handle dot

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits rich text editor min height (#18187)

decrease the min height of the RTE so that it does not have the boundary issue with the max limit when auto height with limits is enabled

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: grammatical error in the help text (#18188)

changed react to reacts in the helpText of the dynamic height property in the proeprty pane

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height tabs double scroll (#18210)

solved the issue by disabling the scroll for the child canvas widget in the tabs widget

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits resizing (#18213)

* fixed the auto height limits resizing issue

* made the auto height overlay independent of isResizing and used its own property to show the grid

* some more refactoring

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* dummy commit

* fix: old apps container issue (#18255)

filtered out the widgets which are detached from layout

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: fixing auto height in childless containers. (#18263)

fixing auto height in childless containers.

* task: Dynamic height reflow fixes in Branch (#18244)

dynamic height reflow fixes

* fix: compact label issue and min and max limits numeric input (#18282)

fixed compact label issue and turned min and max limits to numeric input

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: LabelWithTooltip help icon fix

* fix: NaN and min limit for min and max (#18284)

* fixed compact label issue and turned min and max limits to numeric input

* fixed NaN and set min to be 4

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: validation issues for min max (#18286)

* fixed compact label issue and turned min and max limits to numeric input

* fixed NaN and set min to be 4

* validations start working min max

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* added a full stop to container scroll helper text

* validations start working min max

* dummy commit

* feat: stop resizing auto height widgets vertically because of Drag n Drop Reflow (#18267)

* reflow fixes

* stop resizing auto height widgets vertically because of Drag n Drop Reflow

* feat: Analytics for Dynamic height (#18279)

* Fix canvas min height issue and invisible widgets issue and remove logs and fix issue where widgets overlapped when coming back from preview mode to edit mode

* Fix issue with containers not respecting auto height and decreasing height

* Fix issue with modal widget not hugging contents, and container widgets never become visible after going invisible

* Fix issue where existing containers don't have correct min height for child canvas

* fix: canvasLevelsReducers test (#18301)

fixed the canvasLevelsReducers test

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: removed auto height min max config from widget features (#18316)

removed auto height min max config from widget features

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: Fixing Modal Height updates (#18317)

Fixing Modal Height updates

* fix: text widget background auto height (#18319)

added background color of Text widget back to the auto height container

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* test: cypress tests for auto height (#17676)

* Added tests for dynamic height

* updated tests for another usecase

* moved locators into commonfile

* updated common method

* added tests for some more widgets

* Added tests for jsonForm / Form widget

* Updated the test

* updated test for multiple text widgets

* updated test with few more usecases

* updated the dsl

* updated tests for text change

* updated tests based on new changes

* updated cypress test fixes

* fix: auto height container merge poc wrt release (#18334)

updated the poc wrt PR already merged in the release regarding the auto height container

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: renamed auto height overlay components and added some tests (#18333)

* renamed auto height overlay components and added some tests

* replaced the 10 value with GridDefaults

* avoiding event to reach drop target

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* updated tests

* Merge all code into one branch

* Fix failing AutoHeightcontainer test

* fix: Fix reflow computations which were causing widget overlap (#18300)

* Fix reflow computations which were causing widget overlap

* Fix issues with parent container height and overlapping widgets

* Remove console logs

* Revert comment

* Fix issues related to reflow of containers

* feat: Making getEffectedBoxes a Recursive function in autoHeight Reflow (#18336)

Making getEffectedBoxes a Recursive function in autoHeight Reflow

* Return null for invisible widgets from withWidgetProps

* Remove duplicate import

Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>

* Remove missed console log

* fix: Label position gets deselected on selecting already selected option (#18298)

* fix: Label position gets deselected on selecting the already selected value

* Added migration for Currency & Phone input widgets

* simplify migration function using a utility

* combine conditions

* Increments LATEST_PAGE_VERSION

* Update DynamicHeight_Visibility_spec.js

updated a check wrt auto height

* Handling Modals for canvas size calculations

* fix: migrate label position test failing issue (#18365)

fixed migrate label postition test failing issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* removed the two unwanted imports from DSLMigrations to fix client build

* fix: Auto height zero and limits issue (#18366)

fixed the auto height zero and limits issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: Auto height regression issues (#18367)

* Fix auto height regression issues #18367

* feat: auto height migrations (#18368)

Add auto height migrations

* Increase file caching size

* Use manual array for list of auto height enabled widgets

* Fix cypress test dsl versions

* Revert changes to shouldUpdateHeightDynamically

* Update test results based on code changes

* Marginally increase the workbox file size cache

* review comment incorporated for test spec

* Update container auto height property on drop

* added small wait for validation

Co-authored-by: Ankur Singhal <ankur@appsmith.com>
Co-authored-by: rahulramesha <rahul@appsmith.com>
Co-authored-by: Abhinav Jha <zatanna@Abhinavs-iMac.lan>
Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>
Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Co-authored-by: Albin <albin@appsmith.com>
Co-authored-by: Aswath K <aswath.sana@gmail.com>
Co-authored-by: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com>
Co-authored-by: Apple <nandan@thinkify.io>
2022-11-23 15:18:23 +05:30

826 lines
22 KiB
TypeScript

import {
actionChannel,
ActionPattern,
all,
call,
delay,
fork,
put,
select,
spawn,
take,
} from "redux-saga/effects";
import {
EvaluationReduxAction,
AnyReduxAction,
ReduxAction,
ReduxActionType,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import {
getDataTree,
getUnevaluatedDataTree,
} from "selectors/dataTreeSelectors";
import { getWidgets } from "sagas/selectors";
import WidgetFactory, { WidgetTypeConfigMap } from "utils/WidgetFactory";
import { GracefulWorkerService } from "utils/WorkerUtil";
import {
EvalError,
EVAL_WORKER_ACTIONS,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import log from "loglevel";
import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
import { Action } from "redux";
import {
EVALUATE_REDUX_ACTIONS,
FIRST_EVAL_REDUX_ACTIONS,
setDependencyMap,
setEvaluatedTree,
shouldLint,
shouldProcessBatchedAction,
} from "actions/evaluationActions";
import {
evalErrorHandler,
handleJSFunctionExecutionErrorLog,
logSuccessfulBindings,
postEvalActionDispatcher,
updateTernDefinitions,
} from "./PostEvaluationSagas";
import { JSAction } from "entities/JSCollection";
import { getAppMode } from "selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { get, isUndefined } from "lodash";
import {
setEvaluatedArgument,
setEvaluatedSnippet,
setGlobalSearchFilterContext,
} from "actions/globalSearchActions";
import {
executeActionTriggers,
TriggerMeta,
} from "./ActionExecution/ActionExecutionSagas";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { Toaster, Variant } from "design-system";
import {
createMessage,
SNIPPET_EXECUTION_FAILED,
SNIPPET_EXECUTION_SUCCESS,
} from "@appsmith/constants/messages";
import { validate } from "workers/Evaluation/validations";
import { diff } from "deep-diff";
import { REPLAY_DELAY } from "entities/Replay/replayUtils";
import { EvaluationVersion } from "api/ApplicationApi";
import { makeUpdateJSCollection } from "sagas/JSPaneSagas";
import {
ENTITY_TYPE,
LogObject,
UserLogObject,
} from "entities/AppsmithConsole";
import { Replayable } from "entities/Replay/ReplayEntity/ReplayEditor";
import {
logActionExecutionError,
UncaughtPromiseError,
} from "sagas/ActionExecution/errorUtils";
import { Channel } from "redux-saga";
import { ActionDescription } from "entities/DataTree/actionTriggers";
import { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer";
import { FormEvalActionPayload } from "./FormEvaluationSaga";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { updateMetaState } from "actions/metaActions";
import { getAllActionValidationConfig } from "selectors/entitiesSelector";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { AppTheme } from "entities/AppTheming";
import { ActionValidationConfigMap } from "constants/PropertyControlConstants";
import { storeLogs, updateTriggerMeta } from "./DebuggerSagas";
import { lintTreeSaga, lintWorker } from "./LintingSagas";
import {
EvalTreeRequestData,
EvalTreeResponseData,
} from "workers/Evaluation/types";
const evalWorker = new GracefulWorkerService(
new Worker(
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
{
type: "module",
name: "evalWorker",
},
),
);
let widgetTypeConfigMap: WidgetTypeConfigMap;
function* evaluateTreeSaga(
postEvalActions?: Array<AnyReduxAction>,
shouldReplay = true,
requiresLinting = false,
) {
const allActionValidationConfig: {
[actionId: string]: ActionValidationConfigMap;
} = yield select(getAllActionValidationConfig);
const unevalTree: DataTree = yield select(getUnevaluatedDataTree);
const widgets: CanvasWidgetsReduxState = yield select(getWidgets);
const theme: AppTheme = yield select(getSelectedAppTheme);
const appMode: APP_MODE | undefined = yield select(getAppMode);
const isEditMode = appMode === APP_MODE.EDIT;
log.debug({ unevalTree });
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
const evalTreeRequestData: EvalTreeRequestData = {
unevalTree,
widgetTypeConfigMap,
widgets,
theme,
shouldReplay,
allActionValidationConfig,
requiresLinting: isEditMode && requiresLinting,
};
const workerResponse: EvalTreeResponseData = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
evalTreeRequestData,
);
const {
dataTree,
dependencies,
errors,
evalMetaUpdates = [],
evaluationOrder,
jsUpdates,
logs,
userLogs,
unEvalUpdates,
isCreateFirstTree = false,
} = workerResponse;
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
const oldDataTree: DataTree = yield select(getDataTree);
const updates = diff(oldDataTree, dataTree) || [];
yield put(setEvaluatedTree(updates));
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
// if evalMetaUpdates are present only then dispatch updateMetaState
if (evalMetaUpdates.length) {
yield put(updateMetaState(evalMetaUpdates));
}
log.debug({ evalMetaUpdatesLength: evalMetaUpdates.length });
const updatedDataTree: DataTree = yield select(getDataTree);
if (
!(!isCreateFirstTree && Object.keys(jsUpdates).length > 0) &&
!!userLogs &&
userLogs.length > 0
) {
yield all(
userLogs.map((log: UserLogObject) => {
return call(
storeLogs,
log.logObject,
log.source.name,
log.source.type,
log.source.id,
);
}),
);
}
log.debug({ jsUpdates: jsUpdates });
log.debug({ dataTree: updatedDataTree });
logs?.forEach((evalLog: any) => log.debug(evalLog));
// Added type as any due to https://github.com/redux-saga/redux-saga/issues/1482
yield call(evalErrorHandler as any, errors, updatedDataTree, evaluationOrder);
if (appMode !== APP_MODE.PUBLISHED) {
yield call(makeUpdateJSCollection, jsUpdates);
yield fork(
logSuccessfulBindings,
unevalTree,
updatedDataTree,
evaluationOrder,
isCreateFirstTree,
);
yield fork(updateTernDefinitions, updatedDataTree, unEvalUpdates);
}
yield put(setDependencyMap(dependencies));
if (postEvalActions && postEvalActions.length) {
yield call(postEvalActionDispatcher, postEvalActions);
}
}
export function* evaluateActionBindings(
bindings: string[],
executionParams: Record<string, any> | string = {},
) {
const workerResponse: { errors: EvalError[]; values: unknown } = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS,
{
bindings,
executionParams,
},
);
const { errors, values } = workerResponse;
yield call(evalErrorHandler, errors);
return values;
}
/*
* Used to evaluate and execute dynamic trigger end to end
* Widget action fields and JS Object run triggers this flow
*
* We start a duplex request with the worker and wait till the time we get a 'finished' event from the
* worker. Worker will evaluate a block of code and ask the main thread to execute it. The result of this
* execution is returned to the worker where it can resolve/reject the current promise.
*/
export function* evaluateAndExecuteDynamicTrigger(
dynamicTrigger: string,
eventType: EventType,
triggerMeta: TriggerMeta,
callbackData?: Array<any>,
globalContext?: Record<string, unknown>,
) {
const unEvalTree: DataTree = yield select(getUnevaluatedDataTree);
log.debug({ execute: dynamicTrigger });
const { isFinishedChannel } = yield call(
evalWorker.duplexRequest,
EVAL_WORKER_ACTIONS.EVAL_TRIGGER,
{
dataTree: unEvalTree,
dynamicTrigger,
callbackData,
globalContext,
eventType,
triggerMeta,
},
);
let keepAlive = true;
while (keepAlive) {
const { requestData } = yield take(isFinishedChannel);
log.debug({ requestData, eventType, triggerMeta, dynamicTrigger });
if (requestData.finished) {
keepAlive = false;
const { result } = requestData;
yield call(updateTriggerMeta, triggerMeta, dynamicTrigger);
// Check for any logs in the response and store them in the redux store
if (
!!result &&
result.hasOwnProperty("logs") &&
!!result.logs &&
result.logs.length
) {
yield call(
storeLogs,
result.logs,
triggerMeta.source?.name || triggerMeta.triggerPropertyName || "",
eventType === EventType.ON_JS_FUNCTION_EXECUTE
? ENTITY_TYPE.JSACTION
: ENTITY_TYPE.WIDGET,
triggerMeta.source?.id || "",
);
}
/* Handle errors during evaluation
* A finish event with errors means that the error was not caught by the user code.
* We raise an error telling the user that an uncaught error has occurred
* */
if (
!!result &&
result.hasOwnProperty("errors") &&
!!result.errors &&
result.errors.length
) {
if (
result.errors[0].errorMessage !==
"UncaughtPromiseRejection: User cancelled action execution"
) {
throw new UncaughtPromiseError(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
// need to manually execute these triggers outside the promise flow
const { triggers } = result;
if (triggers && triggers.length) {
log.debug({ triggers });
yield all(
triggers.map((trigger: ActionDescription) =>
call(executeActionTriggers, trigger, eventType, triggerMeta),
),
);
}
// Return value of a promise is returned
isFinishedChannel.close();
return result;
}
yield call(evalErrorHandler, requestData.errors);
isFinishedChannel.close();
}
}
export function* executeDynamicTriggerRequest(
mainThreadRequestChannel: Channel<any>,
) {
while (true) {
const { mainThreadResponseChannel, requestData, requestId } = yield take(
mainThreadRequestChannel,
);
log.debug({ requestData });
if (requestData?.logs) {
const { eventType, triggerMeta } = requestData;
yield call(
storeLogs,
requestData.logs,
triggerMeta?.source?.name || triggerMeta?.triggerPropertyName || "",
eventType === EventType.ON_JS_FUNCTION_EXECUTE
? ENTITY_TYPE.JSACTION
: ENTITY_TYPE.WIDGET,
triggerMeta?.source?.id || "",
);
}
if (requestData?.trigger) {
// if we have found a trigger, we need to execute it and respond back
log.debug({ trigger: requestData.trigger });
yield spawn(
executeTriggerRequestSaga,
requestId,
requestData,
requestData.eventType,
mainThreadResponseChannel,
requestData.triggerMeta,
);
}
if (requestData.type === EVAL_WORKER_ACTIONS.LINT_TREE) {
yield spawn(lintTreeSaga, {
pathsToLint: requestData.lintOrder,
jsUpdates: requestData.jsUpdates,
unevalTree: requestData.unevalTree,
});
}
if (requestData?.errors) {
yield call(evalErrorHandler, requestData.errors);
}
}
}
interface ResponsePayload {
data: {
subRequestId: string;
reason?: string;
resolve?: unknown;
};
success: boolean;
eventType?: EventType;
}
/*
* It is necessary to respond back as the worker is waiting with a pending promise and wanting to know if it should
* resolve or reject it with the data the execution has provided
*/
function* executeTriggerRequestSaga(
requestId: string,
requestData: { trigger: ActionDescription; subRequestId: string },
eventType: EventType,
responseFromExecutionChannel: Channel<unknown>,
triggerMeta: TriggerMeta,
) {
const responsePayload: ResponsePayload = {
data: {
resolve: undefined,
reason: undefined,
subRequestId: requestData.subRequestId,
},
success: false,
eventType,
};
try {
responsePayload.data.resolve = yield call(
executeActionTriggers,
requestData.trigger,
eventType,
triggerMeta,
);
responsePayload.success = true;
} catch (error) {
// When error occurs in execution of triggers,
// a success: false is sent to reject the promise
// @ts-expect-error: reason is of type string
responsePayload.data.reason = { message: error.message };
responsePayload.success = false;
}
responseFromExecutionChannel.put({
method: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
requestId: requestId,
...responsePayload,
});
}
export function* clearEvalCache() {
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE);
return true;
}
export function* executeFunction(
collectionName: string,
action: JSAction,
collectionId: string,
) {
const functionCall = `${collectionName}.${action.name}()`;
const { isAsync } = action.actionConfiguration;
let response: {
errors: any[];
result: any;
logs?: LogObject[];
};
if (isAsync) {
try {
response = yield call(
evaluateAndExecuteDynamicTrigger,
functionCall,
EventType.ON_JS_FUNCTION_EXECUTE,
{
source: {
id: collectionId,
name: `${collectionName}.${action.name}`,
},
triggerPropertyName: `${collectionName}.${action.name}`,
},
);
} catch (e) {
if (e instanceof UncaughtPromiseError) {
logActionExecutionError(e.message);
}
response = { errors: [e], result: undefined };
}
} else {
response = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EXECUTE_SYNC_JS,
{
functionCall,
},
);
const { logs } = response;
// Check for any logs in the response and store them in the redux store
if (!!logs && logs.length > 0) {
yield call(
storeLogs,
logs,
collectionName + "." + action.name,
ENTITY_TYPE.JSACTION,
collectionId,
);
}
}
const { errors, result } = response;
const isDirty = !!errors.length;
yield call(
handleJSFunctionExecutionErrorLog,
collectionId,
collectionName,
action,
errors,
);
return { result, isDirty };
}
export function* validateProperty(
property: string,
value: any,
props: WidgetProps,
) {
const unevalTree: DataTree = yield select(getUnevaluatedDataTree);
// @ts-expect-error: We have a typeMismatch for validationPaths
const validation = unevalTree[props.widgetName].validationPaths[property];
const response: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY,
{
property,
value,
props,
validation,
},
);
return response;
}
function evalQueueBuffer() {
let canTake = false;
let collectedPostEvalActions: any = [];
const take = () => {
if (canTake) {
const resp = collectedPostEvalActions;
collectedPostEvalActions = [];
canTake = false;
return { postEvalActions: resp, type: "BUFFERED_ACTION" };
}
};
const flush = () => {
if (canTake) {
return [take() as Action];
}
return [];
};
const put = (action: EvaluationReduxAction<unknown | unknown[]>) => {
if (!shouldProcessBatchedAction(action)) {
return;
}
canTake = true;
const postEvalActions = getPostEvalActions(action);
collectedPostEvalActions.push(...postEvalActions);
};
return {
take,
put,
isEmpty: () => {
return !canTake;
},
flush,
};
}
/**
* Extract the post eval actions from an evaluation action
* Batched actions have post eval actions inside them, extract that
*
* **/
function getPostEvalActions(
action: EvaluationReduxAction<unknown | unknown[]>,
): AnyReduxAction[] {
const postEvalActions: AnyReduxAction[] = [];
if (action.postEvalActions) {
postEvalActions.push(...action.postEvalActions);
}
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
action.payload.forEach((batchedAction) => {
if (batchedAction.postEvalActions) {
postEvalActions.push(
...(batchedAction.postEvalActions as AnyReduxAction[]),
);
}
});
}
return postEvalActions;
}
function* evaluationChangeListenerSaga() {
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
const [{ mainThreadRequestChannel }] = yield all([
call(evalWorker.start),
call(lintWorker.start),
]);
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP);
yield spawn(executeDynamicTriggerRequest, mainThreadRequestChannel);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
const initAction: {
type: ReduxActionType;
postEvalActions: Array<ReduxAction<unknown>>;
} = yield take(FIRST_EVAL_REDUX_ACTIONS);
yield fork(evaluateTreeSaga, initAction.postEvalActions, false, true);
const evtActionChannel: ActionPattern<Action<any>> = yield actionChannel(
EVALUATE_REDUX_ACTIONS,
evalQueueBuffer(),
);
while (true) {
const action: EvaluationReduxAction<unknown | unknown[]> = yield take(
evtActionChannel,
);
if (shouldProcessBatchedAction(action)) {
const postEvalActions = getPostEvalActions(action);
yield call(
evaluateTreeSaga,
postEvalActions,
get(action, "payload.shouldReplay"),
shouldLint(action),
);
}
}
}
export function* evaluateSnippetSaga(action: any) {
try {
let { expression } = action.payload;
const { dataType, isTrigger } = action.payload;
if (isTrigger) {
expression = `function() { ${expression} }`;
}
const workerResponse: {
errors: any;
result: any;
triggers: any;
} = yield call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_EXPRESSION, {
expression,
dataType,
isTrigger,
});
const { errors, result, triggers } = workerResponse;
if (triggers && triggers.length > 0) {
yield all(
triggers.map((trigger: any) =>
call(
executeActionTriggers,
trigger,
EventType.ON_SNIPPET_EXECUTE,
{},
),
),
);
//Result is when trigger is present. Following code will hide the evaluated snippet section
yield put(setEvaluatedSnippet(result));
} else {
/*
JSON.stringify(undefined) is undefined.
We need to set it manually to "undefined" for codeEditor to display it.
*/
yield put(
setEvaluatedSnippet(
errors?.length
? JSON.stringify(errors, null, 2)
: isUndefined(result)
? "undefined"
: JSON.stringify(result),
),
);
}
Toaster.show({
text: createMessage(
errors?.length ? SNIPPET_EXECUTION_FAILED : SNIPPET_EXECUTION_SUCCESS,
),
variant: errors?.length ? Variant.danger : Variant.success,
});
yield put(
setGlobalSearchFilterContext({
executionInProgress: false,
}),
);
} catch (e) {
yield put(
setGlobalSearchFilterContext({
executionInProgress: false,
}),
);
Toaster.show({
text: createMessage(SNIPPET_EXECUTION_FAILED),
variant: Variant.danger,
});
log.error(e);
Sentry.captureException(e);
}
}
export function* evaluateArgumentSaga(action: any) {
const { name, type, value } = action.payload;
try {
const workerResponse: {
errors: Array<unknown>;
result: unknown;
} = yield call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_EXPRESSION, {
expression: value,
});
const lintErrors = (workerResponse.errors || []).filter(
(error: any) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
if (workerResponse.result) {
const validation = validate({ type }, workerResponse.result, {}, "");
if (!validation.isValid)
validation.messages?.map((message) => {
lintErrors.unshift({
...validation,
...{
errorType: PropertyEvaluationErrorType.VALIDATION,
errorMessage: message,
},
});
});
}
yield put(
setEvaluatedArgument({
[name]: {
type,
value: workerResponse.result,
name,
errors: lintErrors,
isInvalid: lintErrors.length > 0,
},
}),
);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
export function* updateReplayEntitySaga(
actionPayload: ReduxAction<{
entityId: string;
entity: Replayable;
entityType: ENTITY_TYPE;
}>,
) {
//Delay updates to replay object to not persist every keystroke
yield delay(REPLAY_DELAY);
const { entity, entityId, entityType } = actionPayload.payload;
const workerResponse: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT,
{
entityId,
entity,
entityType,
},
);
return workerResponse;
}
export function* workerComputeUndoRedo(operation: string, entityId: string) {
const workerResponse: unknown = yield call(evalWorker.request, operation, {
entityId,
});
return workerResponse;
}
// Type to represent the state of the evaluation reducer
export interface FormEvaluationConfig
extends ReduxAction<FormEvalActionPayload> {
currentEvalState: FormEvaluationState;
}
// Function to trigger the form eval job in the worker
export function* evalFormConfig(formEvaluationConfigObj: FormEvaluationConfig) {
const workerResponse: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.INIT_FORM_EVAL,
formEvaluationConfigObj,
);
return workerResponse;
}
export function* setAppVersionOnWorkerSaga(action: {
type: ReduxActionType;
payload: EvaluationVersion;
}) {
const version: EvaluationVersion = action.payload;
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION, {
version,
});
}
export default function* evaluationSagaListeners() {
yield take(ReduxActionTypes.START_EVALUATION);
while (true) {
try {
yield call(evaluationChangeListenerSaga);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
}