Show JS eval errors in evaluated value pane and debugger (#4463)

Co-authored-by: jsartisan <pawankumar2901@gmail.com>
Co-authored-by: hetunandu <hetu@appsmith.com>
This commit is contained in:
Apeksha Bhosale 2021-05-26 18:02:43 +05:30 committed by GitHub
parent caf7f3678c
commit 4825ce2a2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 183 additions and 216 deletions

View File

@ -215,8 +215,6 @@ const enumTypeGetter = (
type ActionCreatorProps = {
value: string;
isValid: boolean;
validationMessage?: string;
onValueChange: (newValue: string) => void;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
};
@ -263,8 +261,6 @@ type SelectorViewProps = ViewProps & {
type KeyValueViewProps = ViewProps;
type TextViewProps = ViewProps & {
isValid: boolean;
validationMessage?: string;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
};
@ -307,10 +303,8 @@ const views = {
{props.label && <label>{props.label}</label>}
<InputText
additionalAutocomplete={props.additionalAutoComplete}
errorMessage={props.validationMessage}
evaluatedValue={props.get(props.value, false) as string}
expected={"string"}
isValid={props.isValid}
label={props.label}
onChange={(event: any) => {
if (event.target) {
@ -769,8 +763,6 @@ function renderField(props: {
value: string;
field: any;
label?: string;
isValid: boolean;
validationMessage?: string;
apiOptionTree: TreeDropdownOption[];
widgetOptionTree: TreeDropdownOption[];
queryOptionTree: TreeDropdownOption[];
@ -944,8 +936,6 @@ function renderField(props: {
props.onValueChange(finalValueToSet);
},
value: props.value,
isValid: props.isValid,
validationMessage: props.validationMessage,
additionalAutoComplete: props.additionalAutoComplete,
});
break;
@ -961,8 +951,6 @@ function Fields(props: {
value: string;
fields: any;
label?: string;
isValid: boolean;
validationMessage?: string;
apiOptionTree: TreeDropdownOption[];
widgetOptionTree: TreeDropdownOption[];
queryOptionTree: TreeDropdownOption[];
@ -995,7 +983,6 @@ function Fields(props: {
apiOptionTree={props.apiOptionTree}
depth={props.depth + 1}
fields={field}
isValid={props.isValid}
label={selectorField.label}
maxDepth={props.maxDepth}
modalDropdownList={props.modalDropdownList}
@ -1007,7 +994,6 @@ function Fields(props: {
}}
pageDropdownOptions={props.pageDropdownOptions}
queryOptionTree={props.queryOptionTree}
validationMessage={props.validationMessage}
value={selectorField.value}
widgetOptionTree={props.widgetOptionTree}
/>
@ -1039,7 +1025,6 @@ function Fields(props: {
apiOptionTree={props.apiOptionTree}
depth={props.depth + 1}
fields={field}
isValid={props.isValid}
key={index}
label={selectorField.label}
maxDepth={props.maxDepth}
@ -1052,7 +1037,6 @@ function Fields(props: {
}}
pageDropdownOptions={props.pageDropdownOptions}
queryOptionTree={props.queryOptionTree}
validationMessage={props.validationMessage}
value={selectorField.value}
widgetOptionTree={props.widgetOptionTree}
/>
@ -1212,13 +1196,11 @@ export function ActionCreator(props: ActionCreatorProps) {
apiOptionTree={apiOptionTree}
depth={1}
fields={fields}
isValid={props.isValid}
maxDepth={1}
modalDropdownList={modalDropdownList}
onValueChange={props.onValueChange}
pageDropdownOptions={pageDropdownOptions}
queryOptionTree={queryOptionTree}
validationMessage={props.validationMessage}
value={props.value}
widgetOptionTree={widgetOptionTree}
/>

View File

@ -116,6 +116,7 @@ interface Props {
useValidationMessage?: boolean;
hideEvaluatedValue?: boolean;
evaluationSubstitutionType?: EvaluationSubstitutionType;
jsError?: string;
}
interface PopoverContentProps {
@ -129,6 +130,7 @@ interface PopoverContentProps {
onMouseLeave: () => void;
hideEvaluatedValue?: boolean;
preparedStatementViewer: boolean;
jsError?: string;
}
const PreparedStatementViewerContainer = styled.span`
@ -276,7 +278,9 @@ function PopoverContent(props: PopoverContentProps) {
{props.hasError && (
<ErrorText>
<span className="t--evaluatedPopup-error">
{props.useValidationMessage && props.error
{props.jsError && props.jsError.length
? props.jsError
: props.useValidationMessage && props.error
? props.error
: `This value does not evaluate to type "${props.expected}". Transform it using JS inside '{{ }}'`}
</span>
@ -339,6 +343,7 @@ function EvaluatedValuePopup(props: Props) {
expected={props.expected}
hasError={props.hasError}
hideEvaluatedValue={props.hideEvaluatedValue}
jsError={props.jsError}
onMouseEnter={() => {
setContentHovered(true);
}}

View File

@ -14,7 +14,7 @@ import "codemirror/addon/mode/multiplex";
import "codemirror/addon/tern/tern.css";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import { WrappedFieldInputProps } from "redux-form";
import _ from "lodash";
import {
DataTree,
@ -72,7 +72,6 @@ export type EditorStyleProps = {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
height?: string | number;
meta?: Partial<WrappedFieldMetaProps>;
showLineNumbers?: boolean;
className?: string;
leftImage?: string;
@ -335,6 +334,38 @@ class CodeEditor extends Component<Props, State> {
});
}
getPropertyValidation = (
dataTree: DataTree,
dataTreePath?: string,
): {
isValid: boolean;
validationMessage?: string;
jsErrorMessage?: string;
} => {
if (!dataTreePath) {
return { isValid: true, validationMessage: "", jsErrorMessage: "" };
}
const isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
const validationMessagePath = dataTreePath.replace(
"evaluatedValues",
"validationMessages",
);
const jsErrorMessagePath = dataTreePath.replace(
"evaluatedValues",
"jsErrorMessages",
);
const isValid = !_.get(dataTree, isValidPath, false);
const validationMessage = _.get(
dataTree,
validationMessagePath,
"",
) as string;
const jsErrorMessage = _.get(dataTree, jsErrorMessagePath, "") as string;
return { isValid, validationMessage, jsErrorMessage };
};
render() {
const {
border,
@ -351,18 +382,23 @@ class CodeEditor extends Component<Props, State> {
hideEvaluatedValue,
hoverInteraction,
input,
meta,
placeholder,
showLightningMenu,
size,
theme,
useValidationMessage,
} = this.props;
const hasError = !!(meta && meta.error);
const {
isValid,
jsErrorMessage,
validationMessage,
} = this.getPropertyValidation(dynamicData, dataTreePath);
const hasError = !isValid || !!jsErrorMessage;
let evaluated = evaluatedValue;
if (dataTreePath) {
evaluated = _.get(dynamicData, dataTreePath);
}
const showEvaluatedValue =
this.state.isFocused &&
("evaluatedValue" in this.props ||
@ -395,13 +431,14 @@ class CodeEditor extends Component<Props, State> {
</Suspense>
)}
<EvaluatedValuePopup
error={meta?.error}
error={validationMessage}
evaluatedValue={evaluated}
evaluationSubstitutionType={evaluationSubstitutionType}
expected={expected}
hasError={hasError}
hideEvaluatedValue={hideEvaluatedValue}
isOpen={showEvaluatedValue}
jsError={jsErrorMessage}
theme={theme || EditorTheme.LIGHT}
useValidationMessage={useValidationMessage}
>

View File

@ -15,9 +15,7 @@ class ActionSelectorControl extends BaseControl<ControlProps> {
return (
<ActionCreator
additionalAutoComplete={this.props.additionalAutoComplete}
isValid={this.props.isValid}
onValueChange={this.handleValueUpdate}
validationMessage={this.props.errorMessage}
value={propertyValue}
/>
);

View File

@ -34,13 +34,14 @@ export interface ControlProps extends ControlData, ControlFunctions {
export interface ControlData
extends Omit<PropertyPaneControlConfig, "additionalAutoComplete"> {
propertyValue?: any;
isValid: boolean;
errorMessage?: string;
expected: string;
expected?: string;
evaluatedValue: any;
validationMessage?: string;
widgetProperties: any;
useValidationMessage?: boolean;
parentPropertyName: string;
parentPropertyValue: unknown;
additionalDynamicData: Record<string, Record<string, unknown>>;
}
export interface ControlFunctions {
onPropertyChange?: (propertyName: string, propertyValue: string) => void;

View File

@ -1,5 +1,5 @@
import React from "react";
import { get, has, isString } from "lodash";
import { get, isString } from "lodash";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
@ -81,10 +81,7 @@ type RenderComponentProps = {
index: string;
item: ChartData;
length: number;
validationMessage: {
data: string;
seriesName: string;
};
dataTreePath: string;
deleteOption: (index: string) => void;
updateOption: (index: string, key: string, value: string) => void;
evaluated: {
@ -96,13 +93,13 @@ type RenderComponentProps = {
function DataControlComponent(props: RenderComponentProps) {
const {
dataTreePath,
deleteOption,
evaluated,
index,
item,
length,
updateOption,
validationMessage,
} = props;
return (
@ -121,6 +118,7 @@ function DataControlComponent(props: RenderComponentProps) {
</ActionHolder>
<StyledOptionControlWrapper orientation={"HORIZONTAL"}>
<CodeEditor
dataTreePath={`${dataTreePath}.seriesName`}
evaluatedValue={evaluated?.seriesName}
expected={"string"}
input={{
@ -147,6 +145,7 @@ function DataControlComponent(props: RenderComponentProps) {
className={"t--property-control-chart-series-data-control"}
>
<CodeEditor
dataTreePath={`${dataTreePath}.data`}
evaluatedValue={evaluated?.data}
expected={`Array<x:string, y:number>`}
input={{
@ -161,12 +160,6 @@ function DataControlComponent(props: RenderComponentProps) {
updateOption(index, "data", value);
},
}}
meta={{
error: has(validationMessage, "data")
? get(validationMessage, "data")
: "",
touched: true,
}}
mode={EditorModes.JSON_WITH_BINDING}
placeholder=""
size={EditorSize.EXTENDED}
@ -186,7 +179,6 @@ class ChartDataControl extends BaseControl<ControlProps> {
: this.props.propertyValue;
const dataLength = Object.keys(chartData).length;
const { validationMessage } = this.props;
const evaluatedValue = this.props.evaluatedValue;
const firstKey = Object.keys(chartData)[0] as string;
@ -201,6 +193,7 @@ class ChartDataControl extends BaseControl<ControlProps> {
return (
<DataControlComponent
dataTreePath={`${this.props.dataTreePath}.${firstKey}`}
deleteOption={this.deleteOption}
evaluated={get(evaluatedValue, `${firstKey}`)}
index={firstKey}
@ -208,7 +201,6 @@ class ChartDataControl extends BaseControl<ControlProps> {
length={1}
theme={this.props.theme}
updateOption={this.updateOption}
validationMessage={get(validationMessage, `${firstKey}`)}
/>
);
}
@ -221,6 +213,7 @@ class ChartDataControl extends BaseControl<ControlProps> {
return (
<DataControlComponent
dataTreePath={`${this.props.dataTreePath}.${key}`}
deleteOption={this.deleteOption}
evaluated={get(evaluatedValue, `${key}`)}
index={key}
@ -229,7 +222,6 @@ class ChartDataControl extends BaseControl<ControlProps> {
length={dataLength}
theme={this.props.theme}
updateOption={this.updateOption}
validationMessage={get(validationMessage, `${key}`)}
/>
);
})}

View File

@ -14,23 +14,20 @@ class CodeEditorControl extends BaseControl<ControlProps> {
dataTreePath,
evaluatedValue,
expected,
isValid,
propertyValue,
useValidationMessage,
validationMessage,
} = this.props;
const props: Partial<ControlProps> = {};
if (dataTreePath) props.dataTreePath = dataTreePath;
if (evaluatedValue) props.evaluatedValue = evaluatedValue;
if (expected) props.expected = expected;
return (
<CodeEditor
additionalDynamicData={this.props.additionalAutoComplete}
input={{ value: propertyValue, onChange: this.onChange }}
meta={{
error: isValid ? "" : validationMessage,
touched: true,
}}
mode={EditorModes.TEXT_WITH_BINDING}
size={EditorSize.EXTENDED}
tabBehaviour={TabBehaviour.INDENT}

View File

@ -52,7 +52,6 @@ class ColumnActionSelectorControl extends BaseControl<
<InputTextWrapper>
<InputText
evaluatedValue={columnAction.label}
isValid
label={columnAction.label}
onChange={this.updateColumnActionLabel.bind(
this,
@ -64,12 +63,10 @@ class ColumnActionSelectorControl extends BaseControl<
</InputTextWrapper>
<Wrapper>
<ActionCreator
isValid={(columnAction as any).isValid}
onValueChange={this.updateColumnActionFunction.bind(
this,
columnAction,
)}
validationMessage={(columnAction as any).message}
value={columnAction.dynamicTrigger}
/>
</Wrapper>

View File

@ -32,8 +32,6 @@ export function InputText(props: {
label: string;
value: string;
onChange: (event: React.ChangeEvent<HTMLTextAreaElement> | string) => void;
isValid: boolean;
errorMessage?: string;
evaluatedValue?: any;
expected?: string;
placeholder?: string;
@ -44,10 +42,8 @@ export function InputText(props: {
const {
additionalDynamicData,
dataTreePath,
errorMessage,
evaluatedValue,
expected,
isValid,
onChange,
placeholder,
theme,
@ -64,10 +60,6 @@ export function InputText(props: {
value: value,
onChange: onChange,
}}
meta={{
error: isValid ? "" : errorMessage,
touched: true,
}}
mode={EditorModes.TEXT_WITH_BINDING}
placeholder={placeholder}
promptMessage={
@ -93,11 +85,9 @@ class ComputeTablePropertyControl extends BaseControl<
dataTreePath,
defaultValue,
expected,
isValid,
label,
propertyValue,
theme,
validationMessage,
} = this.props;
const tableId = this.props.widgetProperties.widgetName;
const value =
@ -121,9 +111,7 @@ class ComputeTablePropertyControl extends BaseControl<
currentRow,
}}
dataTreePath={dataTreePath}
errorMessage={validationMessage}
expected={expected}
isValid={isValid}
label={label}
onChange={this.onTextChange}
theme={theme}

View File

@ -4,20 +4,11 @@ import InputTextControl, { InputText } from "./InputTextControl";
class CustomFusionChartControl extends InputTextControl {
render() {
const expected = "{\n type: string,\n dataSource: Object\n}";
const {
dataTreePath,
isValid,
label,
placeholderText,
propertyValue,
validationMessage,
} = this.props;
const { dataTreePath, label, placeholderText, propertyValue } = this.props;
return (
<InputText
dataTreePath={dataTreePath}
errorMessage={validationMessage}
expected={expected}
isValid={isValid}
label={label}
onChange={this.onTextChange}
placeholder={placeholderText}

View File

@ -14,8 +14,6 @@ export function InputText(props: {
label: string;
value: string;
onChange: (event: React.ChangeEvent<HTMLTextAreaElement> | string) => void;
isValid: boolean;
errorMessage?: string;
evaluatedValue?: any;
expected?: string;
placeholder?: string;
@ -26,11 +24,9 @@ export function InputText(props: {
}) {
const {
dataTreePath,
errorMessage,
evaluatedValue,
expected,
hideEvaluatedValue,
isValid,
onChange,
placeholder,
value,
@ -48,10 +44,6 @@ export function InputText(props: {
value: value,
onChange: onChange,
}}
meta={{
error: isValid ? "" : errorMessage,
touched: true,
}}
mode={EditorModes.TEXT_WITH_BINDING}
placeholder={placeholder}
size={EditorSize.EXTENDED}
@ -70,21 +62,17 @@ class InputTextControl extends BaseControl<InputControlProps> {
defaultValue,
expected,
hideEvaluatedValue,
isValid,
label,
placeholderText,
propertyValue,
validationMessage,
} = this.props;
return (
<InputText
additionalAutocomplete={additionalAutoComplete}
dataTreePath={dataTreePath}
errorMessage={validationMessage}
expected={expected}
hideEvaluatedValue={hideEvaluatedValue}
isValid={isValid}
label={label}
onChange={this.onTextChange}
placeholder={placeholderText}

View File

@ -4,6 +4,7 @@ enum LOG_TYPE {
ACTION_EXECUTION_ERROR,
ACTION_EXECUTION_SUCCESS,
ENTITY_DELETED,
EVAL_ERROR,
}
export default LOG_TYPE;

View File

@ -1,77 +0,0 @@
import WIDGET_CONFIG_RESPONSE from "./WidgetConfigResponse";
describe("WidgetConfigResponse", () => {
it("it tests autocomplete child enhancements", () => {
const mockProps = {
childAutoComplete: "child-autocomplet",
};
expect(
WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.autocomplete(
mockProps,
),
).toBe(mockProps.childAutoComplete);
});
it("it tests hideEvaluatedValue child enhancements", () => {
expect(
WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.hideEvaluatedValue(),
).toBe(true);
});
it("it tests propertyUpdateHook child enhancements with undefined parent widget", () => {
const mockParentWidget = {
widgetId: undefined,
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([]);
});
it("it tests propertyUpdateHook child enhancements with undefined parent widget", () => {
const mockParentWidget = {
widgetId: undefined,
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([]);
});
it("it tests propertyUpdateHook child enhancements with defined parent widget", () => {
const mockParentWidget = {
widgetId: "parent-widget-id",
widgetName: "parent-widget-name",
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([
{
widgetId: "parent-widget-id",
propertyPath: "template.child-widget-name.text",
propertyValue: "{{parent-widget-name.items.map((currentItem) => )}}",
isDynamicTrigger: false,
},
]);
});
});

View File

@ -664,7 +664,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
autocomplete: (parentProps: any) => {
return parentProps.childAutoComplete;
},
hideEvaluatedValue: () => true,
updateDataTreePath: (parentProps: any, dataTreePath: string) => {
return `${
parentProps.widgetName
}.evaluatedValues.template.${dataTreePath.replace(
"evaluatedValues.",
"",
)}`;
},
propertyUpdateHook: (
parentProps: any,
widgetName: string,

View File

@ -38,6 +38,7 @@ import {
useChildWidgetEnhancementFns,
useParentWithEnhancementFn,
} from "sagas/WidgetEnhancementHelpers";
import { ControlData } from "components/propertyControls/BaseControl";
type Props = PropertyPaneControlConfig & {
panel: IPanelProps;
@ -51,12 +52,13 @@ const PropertyControl = memo((props: Props) => {
widgetProperties.widgetId,
);
/** get all child enhancments functions */
/** get all child enhancements functions */
const {
autoCompleteEnhancementFn: childWidgetAutoCompleteEnhancementFn,
customJSControlEnhancementFn: childWidgetCustomJSControlEnhancementFn,
hideEvaluatedValueEnhancementFn: childWidgetHideEvaluatedValueEnhancementFn,
propertyPaneEnhancmentFn: childWidgetPropertyUpdateEnhancementFn,
propertyPaneEnhancementFn: childWidgetPropertyUpdateEnhancementFn,
updateDataTreePathFn: childWidgetDataTreePathEnhancementFn,
} = useChildWidgetEnhancementFns(widgetProperties.widgetId);
const toggleDynamicProperty = useCallback(
@ -242,41 +244,28 @@ const PropertyControl = memo((props: Props) => {
return null;
}
const getPropertyValidation = (
propertyName: string,
): { isValid: boolean; validationMessage?: string } => {
let isValid = true;
let validationMessage = "";
if (widgetProperties) {
isValid = widgetProperties.invalidProps
? !_.has(widgetProperties.invalidProps, propertyName)
: true;
const validationMsgPresent =
widgetProperties.validationMessages &&
_.has(widgetProperties.validationMessages, propertyName);
validationMessage = validationMsgPresent
? _.get(widgetProperties.validationMessages, propertyName)
: "";
}
return { isValid, validationMessage };
};
const { label, propertyName } = props;
if (widgetProperties) {
const propertyValue = _.get(widgetProperties, propertyName);
const dataTreePath: any = `${widgetProperties.widgetName}.evaluatedValues.${propertyName}`;
// get the dataTreePath and apply enhancement if exists
// TODO (hetu) make the dataTreePath the actual path of the property
// and evaluatedValues should not be added by default
let dataTreePath: string | undefined =
props.dataTreePath ||
`${widgetProperties.widgetName}.evaluatedValues.${propertyName}`;
if (childWidgetDataTreePathEnhancementFn) {
dataTreePath = childWidgetDataTreePathEnhancementFn(dataTreePath);
}
const evaluatedValue = _.get(
widgetProperties,
`evaluatedValues.${propertyName}`,
);
const { isValid, validationMessage } = getPropertyValidation(propertyName);
const { additionalAutoComplete, ...rest } = props;
const config = {
const config: ControlData = {
...rest,
isValid,
propertyValue,
validationMessage,
dataTreePath,
evaluatedValue,
widgetProperties,
@ -288,11 +277,12 @@ const PropertyControl = memo((props: Props) => {
additionalDynamicData: {},
};
if (isPathADynamicTrigger(widgetProperties, propertyName)) {
config.isValid = true;
// config.isValid = true;
config.validationMessage = "";
delete config.dataTreePath;
delete config.evaluatedValue;
delete config.expected;
// config.jsErrorMessage = "";
}
const isDynamic: boolean = isPathADynamicProperty(

View File

@ -49,7 +49,8 @@ const debuggerReducer = createReducer(initialState, {
const entityId = action.payload.source.id;
const id =
action.payload.logType === LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR
action.payload.logType === LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR ||
action.payload.logType === LOG_TYPE.EVAL_ERROR
? `${entityId}-${action.payload.source.propertyPath}`
: entityId;
const previousState = get(state.errors, id, {});
@ -73,7 +74,8 @@ const debuggerReducer = createReducer(initialState, {
const entityId = action.payload.source.id;
const isWidgetErrorLog =
action.payload.logType === LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR;
action.payload.logType === LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR ||
action.payload.logType === LOG_TYPE.EVAL_ERROR;
const id = isWidgetErrorLog
? `${entityId}-${action.payload.source.propertyPath}`
: entityId;

View File

@ -933,6 +933,18 @@ function* executePageLoadAction(pageAction: PageAction) {
message += `\nERROR: "${body}"`;
}
AppsmithConsole.error({
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed with status ${response.data.statusCode}`,
source: {
type: ENTITY_TYPE.ACTION,
name: pageAction.name,
id: pageAction.id,
},
state: response.data?.request ?? null,
message: JSON.stringify(body),
});
yield put(
executeActionError({
actionId: pageAction.id,

View File

@ -29,7 +29,12 @@ function* onWidgetUpdateSaga(payload: LogActionPayload) {
const dataTree: DataTree = yield select(getDataTree);
const widget = dataTree[payload.source.name];
if (!isWidget(widget) || !widget.validationMessages) return;
if (
!isWidget(widget) ||
!widget.validationMessages ||
!widget.jsErrorMessages
)
return;
// Ignore canvas widget updates
if (widget.type === WidgetTypes.CANVAS_WIDGET) {
@ -43,14 +48,17 @@ function* onWidgetUpdateSaga(payload: LogActionPayload) {
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) {
if (noError && noJsError) {
delete errors[errorId];
yield put({
@ -123,6 +131,7 @@ function* debuggerLogSaga(action: ReduxAction<Message>) {
yield call(onWidgetUpdateSaga, payload);
yield put(debuggerLog(payload));
return;
case LOG_TYPE.EVAL_ERROR:
case LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR:
if (payload.source && payload.source.propertyPath) {
if (payload.text) {

View File

@ -106,6 +106,12 @@ const evalErrorHandler = (errors: EvalError[]) => {
}
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: {

View File

@ -31,6 +31,7 @@ export enum WidgetEnhancementType {
CUSTOM_CONTROL = "child.customJSControl",
AUTOCOMPLETE = "child.autocomplete",
HIDE_EVALUATED_VALUE = "child.hideEvaluatedValue",
UPDATE_DATA_TREE_PATH = "child.updateDataTreePath",
}
export function getParentWithEnhancementFn(
@ -161,16 +162,18 @@ export function useChildWidgetEnhancementFn(
}
}
type EnhancmentFns = {
propertyPaneEnhancmentFn: any;
type EnhancementFns = {
updateDataTreePathFn: any;
propertyPaneEnhancementFn: any;
autoCompleteEnhancementFn: any;
customJSControlEnhancementFn: any;
hideEvaluatedValueEnhancementFn: any;
};
export function useChildWidgetEnhancementFns(widgetId: string): EnhancmentFns {
const enhancmentFns = {
propertyPaneEnhancmentFn: undefined,
export function useChildWidgetEnhancementFns(widgetId: string): EnhancementFns {
const enhancementFns = {
updateDataTreePathFn: undefined,
propertyPaneEnhancementFn: undefined,
autoCompleteEnhancementFn: undefined,
customJSControlEnhancementFn: undefined,
hideEvaluatedValueEnhancementFn: undefined,
@ -189,8 +192,12 @@ export function useChildWidgetEnhancementFns(widgetId: string): EnhancmentFns {
if (parentWithEnhancementFn) {
// Get the enhancement function based on the enhancementType
// from the configs
const widgetEnhancmentFns = {
propertyPaneEnhancmentFn: getWidgetEnhancementFn(
const widgetEnhancementFns = {
updateDataTreePathFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.UPDATE_DATA_TREE_PATH,
),
propertyPaneEnhancementFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.PROPERTY_UPDATE,
),
@ -208,16 +215,16 @@ export function useChildWidgetEnhancementFns(widgetId: string): EnhancmentFns {
),
};
Object.keys(widgetEnhancmentFns).map((key: string) => {
const enhancementFn = get(widgetEnhancmentFns, `${key}`);
Object.keys(widgetEnhancementFns).map((key: string) => {
const enhancementFn = get(widgetEnhancementFns, `${key}`);
if (parentDataFromDataTree && enhancementFn) {
set(enhancmentFns, `${key}`, (...args: unknown[]) =>
set(enhancementFns, `${key}`, (...args: unknown[]) =>
enhancementFn(parentDataFromDataTree, ...args),
);
}
});
}
return enhancmentFns;
return enhancementFns;
}

View File

@ -50,9 +50,14 @@ export const getWidgetPropsForPropertyPane = createSelector(
}
if (evaluatedWidget.invalidProps) {
const { invalidProps, validationMessages } = evaluatedWidget;
const {
invalidProps,
jsErrorMessages,
validationMessages,
} = evaluatedWidget;
widgetProperties.invalidProps = invalidProps;
widgetProperties.validationMessages = validationMessages;
widgetProperties.jsErrorMessages = jsErrorMessages;
}
}
return widgetProperties;

View File

@ -158,6 +158,7 @@ export interface WidgetEvaluatedProps {
invalidProps?: Record<string, boolean>;
validationMessages?: Record<string, string>;
evaluatedValues?: Record<string, any>;
jsErrorMessages?: Record<string, string>;
}
export interface EntityWithBindings {

View File

@ -29,6 +29,7 @@ class PropertyControlFactory {
if (customEditor) controlBuilder = this.controlMap.get(customEditor);
else controlBuilder = this.controlMap.get("CODE_EDITOR");
}
if (controlBuilder) {
const controlProps: ControlProps = {
...controlData,

View File

@ -393,6 +393,11 @@ export default class DataTreeEvaluator {
let evalPropertyValue;
const requiresEval =
isABindingPath && isDynamicValue(unEvalPropertyValue);
_.set(
currentTree,
`${entityName}.jsErrorMessages.${propertyPath}`,
"",
);
if (requiresEval) {
const evaluationSubstitutionType =
entity.bindingPaths[propertyPath] ||
@ -403,6 +408,8 @@ export default class DataTreeEvaluator {
currentTree,
evaluationSubstitutionType,
false,
undefined,
fullPropertyPath,
);
} catch (e) {
this.errors.push({
@ -547,6 +554,7 @@ export default class DataTreeEvaluator {
evaluationSubstitutionType: EvaluationSubstitutionType,
returnTriggers: boolean,
callBackData?: Array<any>,
fullPropertyPath?: string,
) {
// Get the {{binding}} bound values
const { jsSnippets, stringSegments } = getDynamicBindings(dynamicBinding);
@ -555,6 +563,7 @@ export default class DataTreeEvaluator {
data,
jsSnippets[0],
callBackData,
fullPropertyPath,
);
return result.triggers;
}
@ -566,6 +575,7 @@ export default class DataTreeEvaluator {
data,
jsSnippet,
callBackData,
fullPropertyPath,
);
return result.result;
} else {
@ -592,17 +602,32 @@ export default class DataTreeEvaluator {
data: DataTree,
js: string,
callbackData?: Array<any>,
fullPropertyPath?: string,
): EvalResult {
try {
return evaluate(js, data, callbackData);
} catch (e) {
this.errors.push({
type: EvalErrorTypes.EVAL_ERROR,
message: e.message,
context: {
binding: js,
},
});
if (fullPropertyPath) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
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,
},
},
});
}
}
return { result: undefined, triggers: [] };
}
}
@ -624,6 +649,7 @@ export default class DataTreeEvaluator {
EvaluationSubstitutionType.TEMPLATE,
true,
undefined,
fullPropertyPath,
);
valueToValidate = triggers;
}
@ -642,7 +668,8 @@ export default class DataTreeEvaluator {
: transformed;
const safeEvaluatedValue = removeFunctions(evaluatedValue);
_.set(widget, `evaluatedValues.${propertyPath}`, safeEvaluatedValue);
if (!isValid) {
const jsError = _.get(widget, `jsErrorMessages.${propertyPath}`);
if (!isValid && !jsError) {
this.errors.push({
type: EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR,
message: message || "",