From 179d5ae8a95f998c00aee5bd375f73d807fa17be Mon Sep 17 00:00:00 2001 From: Apeksha Bhosale <7846888+ApekshaBhosale@users.noreply.github.com> Date: Fri, 4 Jun 2021 12:39:36 +0530 Subject: [PATCH] Improve JS error reporting in the debugger (#4854) --- .../editorComponents/CodeEditor/index.tsx | 37 +++-- .../formControls/DynamicInputTextControl.tsx | 2 +- .../src/entities/AppsmithConsole/logtype.ts | 1 + .../src/entities/DataTree/dataTreeAction.ts | 1 + .../src/entities/DataTree/dataTreeFactory.ts | 3 + .../entities/DataTree/dataTreeWidget.test.ts | 4 + .../src/entities/DataTree/dataTreeWidget.ts | 8 + .../mockResponses/WidgetConfigResponse.tsx | 29 ++++ app/client/src/sagas/ActionSagas.ts | 8 +- app/client/src/sagas/DebuggerSagas.ts | 107 +------------ app/client/src/sagas/EvaluationsSaga.ts | 151 ++++++++++++++---- app/client/src/selectors/editorSelectors.tsx | 1 + app/client/src/utils/DynamicBindingUtils.ts | 4 +- app/client/src/utils/WidgetPropsUtils.tsx | 45 +++++- app/client/src/workers/DataTreeEvaluator.ts | 51 ++---- app/client/src/workers/evaluation.test.ts | 42 +++-- app/client/src/workers/evaluation.worker.ts | 21 ++- 17 files changed, 302 insertions(+), 213 deletions(-) diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index 00c432efad..e9592e2f15 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -50,12 +50,19 @@ import "codemirror/addon/fold/foldgutter"; import "codemirror/addon/fold/foldgutter.css"; import * as Sentry from "@sentry/react"; import { removeNewLineChars, getInputValue } from "./codeEditorUtils"; +import { getEntityNameAndPropertyPath } from "workers/evaluationUtils"; const LightningMenu = lazy(() => retryPromise(() => import("components/editorComponents/LightningMenu")), ); -const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape", "Comma"]; +const AUTOCOMPLETE_CLOSE_KEY_CODES = [ + "Enter", + "Tab", + "Escape", + "Comma", + "Backspace", +]; interface ReduxStateProps { dynamicData: DataTree; @@ -341,16 +348,26 @@ class CodeEditor extends Component { if (!dataTreePath) { return { isValid: true, validationMessage: "", jsErrorMessage: "" }; } - const isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps"); - const validationMessagePath = dataTreePath.replace( - "evaluatedValues", - "validationMessages", + const { entityName, propertyPath } = getEntityNameAndPropertyPath( + dataTreePath, ); - const jsErrorMessagePath = dataTreePath.replace( - "evaluatedValues", - "jsErrorMessages", - ); - + let isValidPath, validationMessagePath, jsErrorMessagePath; + if (dataTreePath && dataTreePath.match(/evaluatedValues/g)) { + isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps"); + validationMessagePath = dataTreePath.replace( + "evaluatedValues", + "validationMessages", + ); + jsErrorMessagePath = dataTreePath.replace( + "evaluatedValues", + "jsErrorMessages", + ); + } else { + isValidPath = entityName + "invalidProps" + propertyPath; + validationMessagePath = + entityName + ".validationMessages." + propertyPath; + jsErrorMessagePath = entityName + ".jsErrorMessages." + propertyPath; + } const isValid = !_.get(dataTree, isValidPath, false); const validationMessage = _.get( dataTree, diff --git a/app/client/src/components/formControls/DynamicInputTextControl.tsx b/app/client/src/components/formControls/DynamicInputTextControl.tsx index 764688e3a2..d6caaf97c3 100644 --- a/app/client/src/components/formControls/DynamicInputTextControl.tsx +++ b/app/client/src/components/formControls/DynamicInputTextControl.tsx @@ -20,7 +20,7 @@ export function InputText(props: { const dataTreePath = actionPathFromName(actionName, name); return ( -
+
{label} {isRequired && "*"} diff --git a/app/client/src/entities/AppsmithConsole/logtype.ts b/app/client/src/entities/AppsmithConsole/logtype.ts index dfbe511f23..8fd1658e13 100644 --- a/app/client/src/entities/AppsmithConsole/logtype.ts +++ b/app/client/src/entities/AppsmithConsole/logtype.ts @@ -5,6 +5,7 @@ enum LOG_TYPE { ACTION_EXECUTION_SUCCESS, ENTITY_DELETED, EVAL_ERROR, + ACTION_UPDATE, } export default LOG_TYPE; diff --git a/app/client/src/entities/DataTree/dataTreeAction.ts b/app/client/src/entities/DataTree/dataTreeAction.ts index da13794861..d108491405 100644 --- a/app/client/src/entities/DataTree/dataTreeAction.ts +++ b/app/client/src/entities/DataTree/dataTreeAction.ts @@ -45,5 +45,6 @@ export const generateDataTreeAction = ( isLoading: action.isLoading, bindingPaths: getBindingPathsOfAction(action.config, editorConfig), dependencyMap, + logBlackList: {}, }; }; diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index ea142ce8ae..a1831c2632 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -56,6 +56,8 @@ export interface DataTreeAction bindingPaths: Record; ENTITY_TYPE: ENTITY_TYPE.ACTION; dependencyMap: DependencyMap; + jsErrorMessages?: Record; + logBlackList: Record; } export interface DataTreeWidget extends WidgetProps { @@ -63,6 +65,7 @@ export interface DataTreeWidget extends WidgetProps { triggerPaths: Record; validationPaths: Record; ENTITY_TYPE: ENTITY_TYPE.WIDGET; + logBlackList: Record; } export interface DataTreeAppsmith extends Omit { diff --git a/app/client/src/entities/DataTree/dataTreeWidget.test.ts b/app/client/src/entities/DataTree/dataTreeWidget.test.ts index 5ee2df1219..38dcb236f5 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.test.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.test.ts @@ -221,6 +221,10 @@ describe("generateDataTreeWidget", () => { key: "value", }, ], + logBlackList: { + isValid: true, + value: true, + }, value: "{{Input1.text}}", isDirty: true, isFocused: false, diff --git a/app/client/src/entities/DataTree/dataTreeWidget.ts b/app/client/src/entities/DataTree/dataTreeWidget.ts index fa77cfa3fb..86ca7318cb 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.ts @@ -45,6 +45,10 @@ export const generateDataTreeWidget = ( unInitializedDefaultProps[propertyName] = undefined; } }); + const blockedDerivedProps: Record = {}; + Object.keys(derivedProps).forEach((propertyName) => { + blockedDerivedProps[propertyName] = true; + }); const { bindingPaths, triggerPaths, @@ -62,6 +66,10 @@ export const generateDataTreeWidget = ( ...derivedProps, ...unInitializedDefaultProps, dynamicBindingPathList, + logBlackList: { + ...widget.logBlackList, + ...blockedDerivedProps, + }, bindingPaths, triggerPaths, validationPaths, diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index a189560f6b..b8b1da984e 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -886,6 +886,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { widgets: { [widgetId: string]: FlattenedWidgetProps }, ) => { let template = {}; + const logBlackListMap: any = {}; const container = get( widgets, `${get(widget, "children.0.children.0")}`, @@ -901,6 +902,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { canvas.children && get(canvas, "children", []).forEach((child: string) => { const childWidget = cloneDeep(get(widgets, `${child}`)); + const logBlackList: { [key: string]: boolean } = {}; const keys = Object.keys(childWidget); for (let i = 0; i < keys.length; i++) { @@ -927,6 +929,12 @@ const WidgetConfigResponse: WidgetConfigReducerState = { } } + Object.keys(childWidget).map((key) => { + logBlackList[key] = true; + }); + + logBlackListMap[childWidget.widgetId] = logBlackList; + template = { ...template, [childWidget.widgetName]: childWidget, @@ -945,6 +953,18 @@ const WidgetConfigResponse: WidgetConfigReducerState = { propertyValue: template, }, ]; + + // add logBlackList to updateProperyMap for all children + updatePropertyMap = updatePropertyMap.concat( + Object.keys(logBlackListMap).map((logBlackListMapKey) => { + return { + widgetId: logBlackListMapKey, + propertyName: "logBlackList", + propertyValue: logBlackListMap[logBlackListMapKey], + }; + }), + ); + return updatePropertyMap; }, }, @@ -961,6 +981,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { if (!parentId) return { widgets }; const widget = { ...widgets[widgetId] }; const parent = { ...widgets[parentId] }; + const logBlackList: { [key: string]: boolean } = {}; const disallowedWidgets = [WidgetTypes.FILE_PICKER_WIDGET]; @@ -998,7 +1019,15 @@ const WidgetConfigResponse: WidgetConfigReducerState = { parent.template = template; + // add logBlackList for the children being added + Object.keys(widget).map((key) => { + logBlackList[key] = true; + }); + + widget.logBlackList = logBlackList; + widgets[parentId] = parent; + widgets[widgetId] = widget; return { widgets }; }, diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index f01caaf990..e630d3476c 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -624,15 +624,21 @@ function* setActionPropertySaga(action: ReduxAction) { if (propertyName === "name") return; const actionObj = yield select(getAction, actionId); + const fieldToBeUpdated = propertyName.replace( + "actionConfiguration", + "config", + ); AppsmithConsole.info({ + logType: LOG_TYPE.ACTION_UPDATE, text: "Configuration updated", source: { type: ENTITY_TYPE.ACTION, name: actionObj.name, id: actionId, + propertyPath: fieldToBeUpdated, }, state: { - [propertyName]: value, + [fieldToBeUpdated]: value, }, }); diff --git a/app/client/src/sagas/DebuggerSagas.ts b/app/client/src/sagas/DebuggerSagas.ts index db2122159d..a8ddde6731 100644 --- a/app/client/src/sagas/DebuggerSagas.ts +++ b/app/client/src/sagas/DebuggerSagas.ts @@ -1,73 +1,12 @@ import { debuggerLog, errorLog, updateErrorLog } from "actions/debuggerActions"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; -import { WidgetTypes } from "constants/WidgetConstants"; import { LogActionPayload, Message } from "entities/AppsmithConsole"; -import { - all, - put, - takeEvery, - select, - take, - fork, - call, -} from "redux-saga/effects"; -import { getDataTree } from "selectors/dataTreeSelectors"; -import { isEmpty, set, get } from "lodash"; +import { all, call, fork, put, select, takeEvery } from "redux-saga/effects"; +import { set } from "lodash"; import { getDebuggerErrors } from "selectors/debuggerSelectors"; import { getAction } from "selectors/entitiesSelector"; import { Action, PluginType } from "entities/Action"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; -import { isWidget } from "workers/evaluationUtils"; -import { getWidget } from "./selectors"; -import { WidgetProps } from "widgets/BaseWidget"; - -function* onWidgetUpdateSaga(payload: LogActionPayload) { - if (!payload.source) return; - // Wait for data tree update - yield take(ReduxActionTypes.SET_EVALUATED_TREE); - const dataTree: DataTree = yield select(getDataTree); - const widget = dataTree[payload.source.name]; - - if ( - !isWidget(widget) || - !widget.validationMessages || - !widget.jsErrorMessages - ) - return; - - // Ignore canvas widget updates - if (widget.type === WidgetTypes.CANVAS_WIDGET) { - return; - } - const source = payload.source; - - // If widget properties no longer have validation errors update the same - if (payload.state) { - const propertyPath = Object.keys(payload.state)[0]; - - const validationMessages = widget.validationMessages; - const validationMessage = validationMessages[propertyPath]; - const jsErrorMessages = widget.jsErrorMessages; - const jsErrorMessage = jsErrorMessages[propertyPath]; - const errors = yield select(getDebuggerErrors); - const errorId = `${source.id}-${propertyPath}`; - const widgetErrorLog = errors[errorId]; - if (!widgetErrorLog) return; - - const noError = isEmpty(validationMessage); - const noJsError = isEmpty(jsErrorMessage); - - if (noError && noJsError) { - delete errors[errorId]; - - yield put({ - type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS, - payload: errors, - }); - } - } -} function* formatActionRequestSaga(payload: LogActionPayload, request?: any) { if (!payload.source || !payload.state || !request || !request.headers) { @@ -128,7 +67,9 @@ function* debuggerLogSaga(action: ReduxAction) { switch (payload.logType) { case LOG_TYPE.WIDGET_UPDATE: - yield call(onWidgetUpdateSaga, payload); + yield put(debuggerLog(payload)); + return; + case LOG_TYPE.ACTION_UPDATE: yield put(debuggerLog(payload)); return; case LOG_TYPE.EVAL_ERROR: @@ -178,42 +119,6 @@ function* debuggerLogSaga(action: ReduxAction) { } } -// Pass through error list once after on page load actions executions are complete -function* onExecutePageActionsCompleteSaga() { - yield take(ReduxActionTypes.SET_EVALUATED_TREE); - - const dataTree: DataTree = yield select(getDataTree); - const errors = yield select(getDebuggerErrors); - const updatedErrors = { ...errors }; - const errorIds = Object.keys(errors); - - for (const id of errorIds) { - const splits = id.split("-"); - const entityId = splits[0]; - const propertyName = splits[1]; - const widget: WidgetProps | null = yield select(getWidget, entityId); - - if (widget) { - const dataTreeWidget = dataTree[widget.widgetName] as DataTreeWidget; - - if (!get(dataTreeWidget.invalidProps, propertyName, null)) { - delete updatedErrors[id]; - } - } - } - - yield put({ - type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS, - payload: updatedErrors, - }); -} - export default function* debuggerSagasListeners() { - yield all([ - takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga), - takeEvery( - ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE, - onExecutePageActionsCompleteSaga, - ), - ]); + yield all([takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga)]); } diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 4991934f22..947ad83fc3 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -28,29 +28,127 @@ import { WidgetProps } from "widgets/BaseWidget"; import PerformanceTracker, { PerformanceTransactionName, } from "../utils/PerformanceTracker"; -import { Variant } from "components/ads/common"; -import { Toaster } from "components/ads/Toast"; import * as Sentry from "@sentry/react"; import { Action } from "redux"; import _ from "lodash"; +import { ENTITY_TYPE, Message, Severity } from "entities/AppsmithConsole"; +import LOG_TYPE from "entities/AppsmithConsole/logtype"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { AppState } from "reducers"; +import { + getEntityNameAndPropertyPath, + isAction, + isWidget, +} from "workers/evaluationUtils"; +import moment from "moment/moment"; +import { Toaster } from "components/ads/Toast"; +import { Variant } from "components/ads/common"; +import AppsmithConsole from "utils/AppsmithConsole"; +import AnalyticsUtil from "utils/AnalyticsUtil"; import { createMessage, ERROR_EVAL_ERROR_GENERIC, ERROR_EVAL_TRIGGER, } from "constants/messages"; -import AppsmithConsole from "utils/AppsmithConsole"; -import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import AnalyticsUtil from "utils/AnalyticsUtil"; let widgetTypeConfigMap: WidgetTypeConfigMap; const worker = new GracefulWorkerService(Worker); -const evalErrorHandler = (errors: EvalError[]) => { - if (!errors) return; +const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors; + +function getLatestEvalPropertyErrors( + currentDebuggerErrors: Record, + dataTree: DataTree, + evaluationOrder: Array, +) { + const updatedDebuggerErrors: Record = { + ...currentDebuggerErrors, + }; + + for (const evaluatedPath of evaluationOrder) { + const { entityName, propertyPath } = getEntityNameAndPropertyPath( + evaluatedPath, + ); + const entity = dataTree[entityName]; + if (isWidget(entity) || isAction(entity)) { + if (propertyPath in entity.logBlackList) { + continue; + } + const jsError = _.get(entity, `jsErrorMessages.${propertyPath}`, ""); + const validationError = _.get( + entity, + `validationMessages.${propertyPath}`, + "", + ); + const evaluatedValue = _.get( + entity, + `evaluatedValues.${propertyPath}`, + "", + ); + const error = jsError || validationError; + const idField = isWidget(entity) ? entity.widgetId : entity.actionId; + const nameField = isWidget(entity) ? entity.widgetName : entity.name; + const entityType = isWidget(entity) + ? ENTITY_TYPE.WIDGET + : ENTITY_TYPE.ACTION; + const debuggerKey = idField + "-" + propertyPath; + // if dataTree has error but debugger does not -> add + // if debugger has error and data tree has error -> update error + // if debugger has error but data tree does not -> remove + // if debugger or data tree does not have an error -> no change + + if (_.isString(error) && error !== "") { + // Add or update + updatedDebuggerErrors[debuggerKey] = { + logType: LOG_TYPE.EVAL_ERROR, + text: `The value at ${propertyPath} is invalid`, + message: error, + severity: Severity.ERROR, + timestamp: moment().format("hh:mm:ss"), + source: { + id: idField, + name: nameField, + type: entityType, + propertyPath: propertyPath, + }, + state: { + value: evaluatedValue, + }, + }; + } else if (debuggerKey in updatedDebuggerErrors) { + // Remove + delete updatedDebuggerErrors[debuggerKey]; + } + } + } + return updatedDebuggerErrors; +} + +function* evalErrorHandler( + errors: EvalError[], + dataTree?: DataTree, + evaluationOrder?: Array, +): any { + if (dataTree && evaluationOrder) { + const currentDebuggerErrors: Record = yield select( + getDebuggerErrors, + ); + const evalPropertyErrors = getLatestEvalPropertyErrors( + currentDebuggerErrors, + dataTree, + evaluationOrder, + ); + + yield put({ + type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS, + payload: evalPropertyErrors, + }); + } + errors.forEach((error) => { switch (error.type) { - case EvalErrorTypes.DEPENDENCY_ERROR: { + case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: { if (error.context) { // Add more info about node for the toast const { entityType, node } = error.context; @@ -104,33 +202,13 @@ const evalErrorHandler = (errors: EvalError[]) => { }); break; } - case EvalErrorTypes.EVAL_ERROR: { - log.debug(error); - AppsmithConsole.error({ - logType: LOG_TYPE.EVAL_ERROR, - text: `The value at ${error.context?.source.propertyPath} is invalid`, - message: error.message, - source: error.context?.source, - }); - break; - } - case EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR: { - AppsmithConsole.error({ - logType: LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR, - text: `The value at ${error.context?.source.propertyPath} is invalid`, - message: error.message, - source: error.context?.source, - state: error.context?.state, - }); - break; - } default: { Sentry.captureException(error); log.debug(error); } } }); -}; +} function* postEvalActionDispatcher( actions: Array | ReduxActionWithoutPayload>, @@ -156,10 +234,16 @@ function* evaluateTreeSaga( widgetTypeConfigMap, }, ); - const { dataTree, dependencies, errors, logs } = workerResponse; + const { + dataTree, + dependencies, + errors, + evaluationOrder, + logs, + } = workerResponse; log.debug({ dataTree: dataTree }); logs.forEach((evalLog: any) => log.debug(evalLog)); - evalErrorHandler(errors); + yield call(evalErrorHandler, errors, dataTree, evaluationOrder); PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, ); @@ -197,7 +281,7 @@ export function* evaluateActionBindings( const { errors, values } = workerResponse; - evalErrorHandler(errors); + yield call(evalErrorHandler, errors); return values; } @@ -214,7 +298,7 @@ export function* evaluateDynamicTrigger( ); const { errors, triggers } = workerResponse; - evalErrorHandler(errors); + yield call(evalErrorHandler, errors); return triggers; } @@ -302,7 +386,6 @@ const EVALUATE_REDUX_ACTIONS = [ ]; const shouldProcessAction = (action: ReduxAction) => { - // debugger; if ( action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && Array.isArray(action.payload) diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 3203ae1519..97ce93c0e2 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -264,6 +264,7 @@ const createLoadingWidget = ( bindingPaths: {}, triggerPaths: {}, validationPaths: {}, + logBlackList: {}, isLoading: true, }; }; diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index d8cf9ce8e8..30ebd67f4d 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -84,12 +84,10 @@ export const getDynamicBindings = ( }; export enum EvalErrorTypes { - DEPENDENCY_ERROR = "DEPENDENCY_ERROR", + CYCLICAL_DEPENDENCY_ERROR = "CYCLICAL_DEPENDENCY_ERROR", EVAL_PROPERTY_ERROR = "EVAL_PROPERTY_ERROR", WIDGET_PROPERTY_VALIDATION_ERROR = "WIDGET_PROPERTY_VALIDATION_ERROR", EVAL_TREE_ERROR = "EVAL_TREE_ERROR", - UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR", - EVAL_ERROR = "EVAL_ERROR", UNKNOWN_ERROR = "UNKNOWN_ERROR", BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR", EVAL_TRIGGER_ERROR = "EVAL_TRIGGER_ERROR", diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 1f580a6abc..a7f608faf9 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -22,7 +22,7 @@ import defaultTemplate from "templates/default"; import { generateReactKey } from "./generators"; import { ChartDataPoint } from "widgets/ChartWidget"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; -import { has, isString, omit, set } from "lodash"; +import { get, has, isString, omit, set } from "lodash"; import log from "loglevel"; import { migrateTablePrimaryColumnsBindings, @@ -764,6 +764,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = LATEST_PAGE_VERSION; } + if (currentDSL.version === 22) { + currentDSL = addLogBlackListToAllListWidgetChildren(currentDSL); + currentDSL.version = 23; + } + return currentDSL; }; @@ -1164,3 +1169,41 @@ export const generateWidgetProps = ( } else throw Error("Failed to create widget: Parent was not provided "); } }; + +/** + * adds logBlackList key for all list widget children + * + * @param currentDSL + * @returns + */ +const addLogBlackListToAllListWidgetChildren = ( + currentDSL: ContainerWidgetProps, +) => { + currentDSL.children = currentDSL.children?.map((children: WidgetProps) => { + if (children.type === WidgetTypes.LIST_WIDGET) { + const widgets = get( + children, + "children.0.children.0.children.0.children", + ); + + widgets.map((widget: any, index: number) => { + const logBlackList: { [key: string]: boolean } = {}; + + Object.keys(widget).map((key) => { + logBlackList[key] = true; + }); + if (!widget.logBlackList) { + set( + children, + `children.0.children.0.children.0.children.${index}.logBlackList`, + logBlackList, + ); + } + }); + } + + return children; + }); + + return currentDSL; +}; diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index 5ac99562ad..b778feb192 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -112,6 +112,10 @@ export default class DataTreeEvaluator { }, }; this.logs.push({ timeTakenForFirstTree }); + return { + dataTree: this.evalTree, + evaluationOrder: this.sortedDependencies, + }; } isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) { @@ -206,7 +210,10 @@ export default class DataTreeEvaluator { evaluate: (evalStop - evalStart).toFixed(2), }; this.logs.push({ timeTakenForSubTreeEval }); - return this.evalTree; + return { + dataTree: this.evalTree, + evaluationOrder: evaluationOrder, + }; } getCompleteSortOrder( @@ -502,7 +509,7 @@ export default class DataTreeEvaluator { entityType = entity.pluginType; } this.errors.push({ - type: EvalErrorTypes.DEPENDENCY_ERROR, + type: EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR, message: "Cyclic dependency found while evaluating.", context: { node, @@ -612,21 +619,13 @@ export default class DataTreeEvaluator { fullPropertyPath, ); _.set(data, `${entityName}.jsErrorMessages.${propertyPath}`, e.message); - const entity = data[entityName]; - if (isWidget(entity)) { - this.errors.push({ - type: EvalErrorTypes.EVAL_ERROR, - message: e.message, - context: { - source: { - id: entity.widgetId, - name: entity.widgetName, - type: ENTITY_TYPE.WIDGET, - propertyPath: propertyPath, - }, - }, - }); - } + } else { + // TODO clean up + // This is to handle situations with evaluation of triggers for execution + this.errors.push({ + type: EvalErrorTypes.EVAL_PROPERTY_ERROR, + message: e.message, + }); } return { result: undefined, triggers: [] }; } @@ -668,23 +667,7 @@ export default class DataTreeEvaluator { : transformed; const safeEvaluatedValue = removeFunctions(evaluatedValue); _.set(widget, `evaluatedValues.${propertyPath}`, safeEvaluatedValue); - const jsError = _.get(widget, `jsErrorMessages.${propertyPath}`); - if (!isValid && !jsError) { - this.errors.push({ - type: EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR, - message: message || "", - context: { - source: { - id: widget.widgetId, - name: widget.widgetName, - type: ENTITY_TYPE.WIDGET, - propertyPath: propertyPath, - }, - state: { - value: safeEvaluatedValue, - }, - }, - }); + if (!isValid) { _.set(widget, `invalidProps.${propertyPath}`, true); _.set(widget, `validationMessages.${propertyPath}`, message); } else { diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts index 56a3d83843..f257d124ba 100644 --- a/app/client/src/workers/evaluation.test.ts +++ b/app/client/src/workers/evaluation.test.ts @@ -384,9 +384,9 @@ describe("DataTreeEvaluator", () => { text: "Hey there", }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); - expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there"); - expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there"); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); + expect(dataTree).toHaveProperty("Text2.text", "Hey there"); + expect(dataTree).toHaveProperty("Text3.text", "Hey there"); }); it("Evaluates a dependency change in update run", () => { @@ -397,10 +397,10 @@ describe("DataTreeEvaluator", () => { text: "Label 3", }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); const updatedDependencyMap = evaluator.dependencyMap; - expect(updatedEvalTree).toHaveProperty("Text2.text", "Label"); - expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3"); + expect(dataTree).toHaveProperty("Text2.text", "Label"); + expect(dataTree).toHaveProperty("Text3.text", "Label 3"); expect(updatedDependencyMap).toStrictEqual({ Text1: ["Text1.text"], Text2: ["Text2.text"], @@ -445,8 +445,8 @@ describe("DataTreeEvaluator", () => { }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); - expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value"); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); + expect(dataTree).toHaveProperty("Input1.text", "Default value"); }); it("Evaluates for value changes in nested diff paths", () => { @@ -481,11 +481,8 @@ describe("DataTreeEvaluator", () => { }, }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); - expect(updatedEvalTree).toHaveProperty( - "Dropdown2.options.0.label", - "newValue", - ); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); + expect(dataTree).toHaveProperty("Dropdown2.options.0.label", "newValue"); }); it("Adds an entity with a complicated binding", () => { @@ -504,9 +501,9 @@ describe("DataTreeEvaluator", () => { ], }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); const updatedDependencyMap = evaluator.dependencyMap; - expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + expect(dataTree).toHaveProperty("Table1.tableData", [ { test: "Hey", raw: "Label", @@ -568,9 +565,9 @@ describe("DataTreeEvaluator", () => { ], }, }; - const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree); const updatedDependencyMap = evaluator.dependencyMap; - expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + expect(dataTree).toHaveProperty("Table1.tableData", [ { test: "Hey", raw: "Label", @@ -580,7 +577,7 @@ describe("DataTreeEvaluator", () => { raw: "Label", }, ]); - expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey"); + expect(dataTree).toHaveProperty("Text4.text", "Hey"); expect(updatedDependencyMap).toStrictEqual({ Api1: ["Api1.data"], Text1: ["Text1.text"], @@ -657,14 +654,14 @@ describe("DataTreeEvaluator", () => { }, }, }; - const evaluatedDataTree2 = evaluator.updateDataTree(updatedTree2); + const { dataTree } = evaluator.updateDataTree(updatedTree2); expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ "Text1.text", "Api2.config.pluginSpecifiedTemplates[0].value", ]); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(evaluatedDataTree2.Api2.config.body).toBe("{ 'name': Test }"); + expect(dataTree.Api2.config.body).toBe("{ 'name': Test }"); const updatedTree3 = { ...updatedTree2, Api2: { @@ -683,13 +680,14 @@ describe("DataTreeEvaluator", () => { }, }, }; - const evaluatedDataTree3 = evaluator.updateDataTree(updatedTree3); + const evaluatedDataTreeObject = evaluator.updateDataTree(updatedTree3); + const dataTree3 = evaluatedDataTreeObject.dataTree; expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ "Text1.text", "Api2.config.pluginSpecifiedTemplates[0].value", ]); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(evaluatedDataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }"); + expect(dataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }"); }); }); diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index f08f4f2c30..42d5477c78 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -47,18 +47,24 @@ ctx.addEventListener( let errors: EvalError[] = []; let logs: any[] = []; let dependencies: DependencyMap = {}; + let dataTreeObject: any = {}; + let evaluationOrder: Array = []; try { if (!dataTreeEvaluator) { dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap); - dataTreeEvaluator.createFirstTree(unevalTree); - dataTree = dataTreeEvaluator.evalTree; + dataTreeObject = dataTreeEvaluator.createFirstTree(unevalTree); + dataTree = dataTreeObject.dataTree; + evaluationOrder = dataTreeObject.evaluationOrder; + // dataTreeEvaluator.sortedDepedencies } else { - dataTree = dataTreeEvaluator.updateDataTree(unevalTree); + dataTreeObject = dataTreeEvaluator.updateDataTree(unevalTree); + dataTree = dataTreeObject.dataTree; + evaluationOrder = dataTreeObject.evaluationOrder; } // We need to clean it to remove any possible functions inside the tree. // If functions exist, it will crash the web worker - dataTree = JSON.parse(JSON.stringify(dataTree)); + dataTree = dataTree && JSON.parse(JSON.stringify(dataTree)); dependencies = dataTreeEvaluator.inverseDependencyMap; errors = dataTreeEvaluator.errors; dataTreeEvaluator.clearErrors(); @@ -79,10 +85,12 @@ ctx.addEventListener( dataTree = getSafeToRenderDataTree(unevalTree, widgetTypeConfigMap); dataTreeEvaluator = undefined; } + // step 6: eval order return { dataTree, dependencies, errors, + evaluationOrder, logs, }; } @@ -108,7 +116,8 @@ ctx.addEventListener( if (!dataTreeEvaluator) { return { triggers: [], errors: [] }; } - const evalTree = dataTreeEvaluator.updateDataTree(dataTree); + dataTreeEvaluator.updateDataTree(dataTree); + const evalTree = dataTreeEvaluator.evalTree; const triggers = dataTreeEvaluator.getDynamicValue( dynamicTrigger, evalTree, @@ -120,7 +129,7 @@ ctx.addEventListener( // Transforming eval errors into eval trigger errors. Since trigger // errors occur less, we want to treat it separately const errors = dataTreeEvaluator.errors.map((error) => { - if (error.type === EvalErrorTypes.EVAL_ERROR) { + if (error.type === EvalErrorTypes.EVAL_PROPERTY_ERROR) { return { ...error, type: EvalErrorTypes.EVAL_TRIGGER_ERROR,