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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ enum LOG_TYPE {
ACTION_EXECUTION_ERROR, ACTION_EXECUTION_ERROR,
ACTION_EXECUTION_SUCCESS, ACTION_EXECUTION_SUCCESS,
ENTITY_DELETED, ENTITY_DELETED,
EVAL_ERROR,
} }
export default LOG_TYPE; 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) => { autocomplete: (parentProps: any) => {
return parentProps.childAutoComplete; return parentProps.childAutoComplete;
}, },
hideEvaluatedValue: () => true, updateDataTreePath: (parentProps: any, dataTreePath: string) => {
return `${
parentProps.widgetName
}.evaluatedValues.template.${dataTreePath.replace(
"evaluatedValues.",
"",
)}`;
},
propertyUpdateHook: ( propertyUpdateHook: (
parentProps: any, parentProps: any,
widgetName: string, widgetName: string,

View File

@ -38,6 +38,7 @@ import {
useChildWidgetEnhancementFns, useChildWidgetEnhancementFns,
useParentWithEnhancementFn, useParentWithEnhancementFn,
} from "sagas/WidgetEnhancementHelpers"; } from "sagas/WidgetEnhancementHelpers";
import { ControlData } from "components/propertyControls/BaseControl";
type Props = PropertyPaneControlConfig & { type Props = PropertyPaneControlConfig & {
panel: IPanelProps; panel: IPanelProps;
@ -51,12 +52,13 @@ const PropertyControl = memo((props: Props) => {
widgetProperties.widgetId, widgetProperties.widgetId,
); );
/** get all child enhancments functions */ /** get all child enhancements functions */
const { const {
autoCompleteEnhancementFn: childWidgetAutoCompleteEnhancementFn, autoCompleteEnhancementFn: childWidgetAutoCompleteEnhancementFn,
customJSControlEnhancementFn: childWidgetCustomJSControlEnhancementFn, customJSControlEnhancementFn: childWidgetCustomJSControlEnhancementFn,
hideEvaluatedValueEnhancementFn: childWidgetHideEvaluatedValueEnhancementFn, hideEvaluatedValueEnhancementFn: childWidgetHideEvaluatedValueEnhancementFn,
propertyPaneEnhancmentFn: childWidgetPropertyUpdateEnhancementFn, propertyPaneEnhancementFn: childWidgetPropertyUpdateEnhancementFn,
updateDataTreePathFn: childWidgetDataTreePathEnhancementFn,
} = useChildWidgetEnhancementFns(widgetProperties.widgetId); } = useChildWidgetEnhancementFns(widgetProperties.widgetId);
const toggleDynamicProperty = useCallback( const toggleDynamicProperty = useCallback(
@ -242,41 +244,28 @@ const PropertyControl = memo((props: Props) => {
return null; 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; const { label, propertyName } = props;
if (widgetProperties) { if (widgetProperties) {
const propertyValue = _.get(widgetProperties, propertyName); 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( const evaluatedValue = _.get(
widgetProperties, widgetProperties,
`evaluatedValues.${propertyName}`, `evaluatedValues.${propertyName}`,
); );
const { isValid, validationMessage } = getPropertyValidation(propertyName);
const { additionalAutoComplete, ...rest } = props; const { additionalAutoComplete, ...rest } = props;
const config = { const config: ControlData = {
...rest, ...rest,
isValid,
propertyValue, propertyValue,
validationMessage,
dataTreePath, dataTreePath,
evaluatedValue, evaluatedValue,
widgetProperties, widgetProperties,
@ -288,11 +277,12 @@ const PropertyControl = memo((props: Props) => {
additionalDynamicData: {}, additionalDynamicData: {},
}; };
if (isPathADynamicTrigger(widgetProperties, propertyName)) { if (isPathADynamicTrigger(widgetProperties, propertyName)) {
config.isValid = true; // config.isValid = true;
config.validationMessage = ""; config.validationMessage = "";
delete config.dataTreePath; delete config.dataTreePath;
delete config.evaluatedValue; delete config.evaluatedValue;
delete config.expected; delete config.expected;
// config.jsErrorMessage = "";
} }
const isDynamic: boolean = isPathADynamicProperty( const isDynamic: boolean = isPathADynamicProperty(

View File

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

View File

@ -933,6 +933,18 @@ function* executePageLoadAction(pageAction: PageAction) {
message += `\nERROR: "${body}"`; 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( yield put(
executeActionError({ executeActionError({
actionId: pageAction.id, actionId: pageAction.id,

View File

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

View File

@ -106,6 +106,12 @@ const evalErrorHandler = (errors: EvalError[]) => {
} }
case EvalErrorTypes.EVAL_ERROR: { case EvalErrorTypes.EVAL_ERROR: {
log.debug(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; break;
} }
case EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR: { case EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR: {

View File

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

View File

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

View File

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

View File

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

View File

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