diff --git a/app/client/package.json b/app/client/package.json index 7e3e04750f..36769ab170 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -95,6 +95,7 @@ "not op_mini all" ], "devDependencies": { + "@types/jest": "^24.0.22", "@types/react-select": "^3.0.5", "@types/react-tabs": "^2.3.1", "@types/redux-form": "^8.1.9", @@ -106,6 +107,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.14.3", + "react-test-renderer": "^16.11.0", "redux-devtools": "^3.5.0", "redux-devtools-extension": "^2.13.8" }, diff --git a/app/client/src/actions/controlActions.tsx b/app/client/src/actions/controlActions.tsx index e5df6975d0..2716122008 100644 --- a/app/client/src/actions/controlActions.tsx +++ b/app/client/src/actions/controlActions.tsx @@ -3,6 +3,7 @@ import { ReduxAction, } from "../constants/ReduxActionConstants"; import { RenderMode } from "../constants/WidgetConstants"; +import { ErrorCode } from "../constants/validationErrorCodes"; export const updateWidgetProperty = ( widgetId: string, @@ -21,9 +22,28 @@ export const updateWidgetProperty = ( }; }; +export const updateWidgetPropertyValidation = ( + widgetId: string, + propertyName: string, + errorCode: ErrorCode, +): ReduxAction => ({ + type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY_VALIDATION, + payload: { + widgetId, + propertyName, + errorCode, + }, +}); + export interface UpdateWidgetPropertyPayload { widgetId: string; propertyName: string; propertyValue: any; renderMode: RenderMode; } + +export interface UpdateWidgetPropertyValidation { + widgetId: string; + propertyName: string; + errorCode: ErrorCode; +} diff --git a/app/client/src/components/propertyControls/BaseControl.tsx b/app/client/src/components/propertyControls/BaseControl.tsx index 8a6f8d63b5..281086d07d 100644 --- a/app/client/src/components/propertyControls/BaseControl.tsx +++ b/app/client/src/components/propertyControls/BaseControl.tsx @@ -5,6 +5,7 @@ import { Component } from "react"; import _ from "lodash"; import { ControlType } from "../../constants/PropertyControlConstants"; +import { ErrorCode } from "../../constants/validationErrorCodes"; abstract class BaseControl extends Component { updateProperty(propertyName: string, propertyValue: any) { @@ -29,10 +30,13 @@ export interface ControlData { propertyName: string; controlType: ControlType; propertyValue?: any; + propertyError?: ErrorCode; } export interface ControlFunctions { onPropertyChange?: (propertyName: string, propertyValue: string) => void; + getDynamicValue: (dynamicBinding: string) => any; + setPropertyValidation: (propertyName: string, errorCode: ErrorCode) => void; } export default BaseControl; diff --git a/app/client/src/components/propertyControls/InputTextControl.tsx b/app/client/src/components/propertyControls/InputTextControl.tsx index 31d28fd659..f19408931e 100644 --- a/app/client/src/components/propertyControls/InputTextControl.tsx +++ b/app/client/src/components/propertyControls/InputTextControl.tsx @@ -1,8 +1,17 @@ import React from "react"; +import _ from "lodash"; import BaseControl, { ControlProps } from "./BaseControl"; -import { ControlWrapper, StyledInputGroup } from "./StyledControls"; -import { InputType } from "zlib"; +import { + ControlWrapper, + StyledInputGroup, + StyledValidationError, +} from "./StyledControls"; +import { InputType } from "../../widgets/InputWidget"; import { ControlType } from "../../constants/PropertyControlConstants"; +import { isDynamicValue } from "../../utils/DynamicBindingUtils"; +import { ERROR_CODES } from "../../constants/validationErrorCodes"; + +type InputTextControlType = InputType | "OBJECT" | "ARRAY" | "BOOLEAN"; class InputTextControl extends BaseControl { render() { @@ -15,11 +24,16 @@ class InputTextControl extends BaseControl { placeholder={this.props.placeholderText} defaultValue={this.props.propertyValue} /> + {this.props.propertyError && ( + + {this.props.propertyError} + + )} ); } - isNumberType(inputType: InputType): boolean { + isNumberType(inputType: InputTextControlType): boolean { switch (inputType) { case "CURRENCY": case "INTEGER": @@ -31,18 +45,66 @@ class InputTextControl extends BaseControl { } } + isStringType(inputType: InputTextControlType): boolean { + switch (inputType) { + case "TEXT": + case "EMAIL": + case "PASSWORD": + case "SEARCH": + return true; + default: + return false; + } + } + onTextChange = (event: React.ChangeEvent) => { - this.updateProperty(this.props.propertyName, event.target.value); + let value: string | number = event.target.value; + if (this.isNumberType(this.props.inputType)) { + value = _.toNumber(value); + } + this.validateInput(value); + this.updateProperty(this.props.propertyName, value); }; getControlType(): ControlType { return "INPUT_TEXT"; } + + validateInput(inputValue: any): boolean { + const { + getDynamicValue, + inputType, + setPropertyValidation, + propertyName, + } = this.props; + let value = inputValue; + if (isDynamicValue(inputValue)) { + value = getDynamicValue(inputValue); + } + if (this.isNumberType(inputType) && !_.isNumber(value)) { + setPropertyValidation(propertyName, ERROR_CODES.TYPE_ERROR); + return false; + } + if (this.isStringType(inputType) && !_.isString(value)) { + setPropertyValidation(propertyName, ERROR_CODES.TYPE_ERROR); + return false; + } + if (inputType === "ARRAY" && !Array.isArray(value)) { + setPropertyValidation(propertyName, ERROR_CODES.TYPE_ERROR); + return false; + } + if (inputType === "OBJECT" && !_.isObject(value)) { + setPropertyValidation(propertyName, ERROR_CODES.TYPE_ERROR); + return false; + } + setPropertyValidation(propertyName, ERROR_CODES.NO_ERROR); + return true; + } } export interface InputControlProps extends ControlProps { placeholderText: string; - inputType: InputType; + inputType: InputTextControlType; isDisabled?: boolean; } diff --git a/app/client/src/components/propertyControls/StyledControls.tsx b/app/client/src/components/propertyControls/StyledControls.tsx index ba6400e18d..1049014d62 100644 --- a/app/client/src/components/propertyControls/StyledControls.tsx +++ b/app/client/src/components/propertyControls/StyledControls.tsx @@ -76,3 +76,7 @@ export const StyledTimeZonePicker = styled(TimezonePicker)` box-shadow: none; } `; + +export const StyledValidationError = styled.span` + color: ${props => props.theme.colors.error}; +`; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 394175d090..ac788324cd 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -74,6 +74,7 @@ export const ReduxActionTypes: { [key: string]: string } = { CREATE_APPLICATION_SUCCESS: "CREATE_APPLICATION_SUCCESS", CREATE_UPDATE_BINDINGS_MAP_INIT: "CREATE_UPDATE_BINDINGS_MAP_INIT", CREATE_UPDATE_BINDINGS_MAP_SUCCESS: "CREATE_UPDATE_BINDINGS_MAP_SUCCESS", + UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION", HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE", }; diff --git a/app/client/src/constants/validationErrorCodes.ts b/app/client/src/constants/validationErrorCodes.ts new file mode 100644 index 0000000000..780ab16bab --- /dev/null +++ b/app/client/src/constants/validationErrorCodes.ts @@ -0,0 +1,11 @@ +export const ERROR_CODES = { + NO_ERROR: "NO_ERROR", + TYPE_ERROR: "TYPE_ERROR", +}; + +export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; + +export const ERROR_CODES_MESSAGES: Record = { + NO_ERROR: "", + TYPE_ERROR: "This input is not of a valid type", +}; diff --git a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx index 48405a8bc1..6a9ab25ab2 100644 --- a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx +++ b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx @@ -1,6 +1,6 @@ import { PropertyPaneConfigState } from "../reducers/entityReducers/propertyPaneConfigReducer"; -const PropertyPaneConfigResponse: PropertyPaneConfigState = { +const PropertyPaneConfigResponse = { config: { BUTTON_WIDGET: [ { @@ -384,6 +384,7 @@ const PropertyPaneConfigResponse: PropertyPaneConfigState = { propertyName: "tableData", label: "Table Data", controlType: "INPUT_TEXT", + inputType: "ARRAY", }, { id: "11.3", diff --git a/app/client/src/pages/Editor/PropertyPane.tsx b/app/client/src/pages/Editor/PropertyPane.tsx index 3bff756426..5624d72622 100644 --- a/app/client/src/pages/Editor/PropertyPane.tsx +++ b/app/client/src/pages/Editor/PropertyPane.tsx @@ -1,17 +1,21 @@ import React, { Component } from "react"; import styled from "styled-components"; import { connect } from "react-redux"; -import { AppState } from "../../reducers"; +import { AppState, DataTree } from "../../reducers"; import PropertyControlFactory from "../../utils/PropertyControlFactory"; import _ from "lodash"; import { PropertySection } from "../../reducers/entityReducers/propertyPaneConfigReducer"; -import { updateWidgetProperty } from "../../actions/controlActions"; +import { + updateWidgetProperty, + updateWidgetPropertyValidation, +} from "../../actions/controlActions"; import { getCurrentWidgetId, getCurrentReferenceNode, getPropertyConfig, getIsPropertyPaneVisible, getCurrentWidgetProperties, + getPropertyErrors, } from "../../selectors/propertyPaneSelectors"; import { Divider } from "@blueprintjs/core"; @@ -19,6 +23,12 @@ import Popper from "./Popper"; import { ControlProps } from "../../components/propertyControls/BaseControl"; import { RenderModes } from "../../constants/WidgetConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { getDataTree } from "../../selectors/entitiesSelector"; +import { getDynamicValue as extractDynamicValue } from "../../utils/DynamicBindingUtils"; +import { + ErrorCode, + ERROR_CODES_MESSAGES, +} from "../../constants/validationErrorCodes"; import { CloseButton } from "components/designSystems/blueprint/CloseButton"; import { theme } from "../../constants/DefaultTheme"; @@ -52,6 +62,8 @@ class PropertyPane extends Component< constructor(props: PropertyPaneProps & PropertyPaneFunctions) { super(props); this.onPropertyChange = this.onPropertyChange.bind(this); + this.getDynamicValue = this.getDynamicValue.bind(this); + this.setPropertyValidation = this.setPropertyValidation.bind(this); } getPropertyValue = (propertyName: string) => { @@ -63,6 +75,17 @@ class PropertyPane extends Component< return widgetProperties[propertyName]; }; + getPropertyValidation = (propertyName: string): string | undefined => { + const { propertyErrors, widgetId } = this.props; + if (!widgetId) return undefined; + if (widgetId in propertyErrors) { + const errorCode: ErrorCode = propertyErrors[widgetId][propertyName]; + return ERROR_CODES_MESSAGES[errorCode]; + } else { + return undefined; + } + }; + render() { if (this.props.isVisible) { const content = this.renderPropertyPane(this.props.propertySections); @@ -131,9 +154,16 @@ class PropertyPane extends Component< propertyControlOrSection.propertyValue = this.getPropertyValue( propertyControlOrSection.propertyName, ); + propertyControlOrSection.propertyError = this.getPropertyValidation( + propertyControlOrSection.propertyName, + ); return PropertyControlFactory.createControl( propertyControlOrSection, - { onPropertyChange: this.onPropertyChange }, + { + onPropertyChange: this.onPropertyChange, + getDynamicValue: this.getDynamicValue, + setPropertyValidation: this.setPropertyValidation, + }, ); } catch (e) { console.log(e); @@ -153,11 +183,28 @@ class PropertyPane extends Component< propertyValue, ); } + + getDynamicValue(dynamicBinding: string) { + const { dataTree } = this.props; + return extractDynamicValue(dynamicBinding, dataTree); + } + + setPropertyValidation(propertyName: string, errorCode: ErrorCode) { + if (this.props.widgetId) { + this.props.setPropertyValidation( + this.props.widgetId, + propertyName, + errorCode, + ); + } + } } const mapStateToProps = (state: AppState): PropertyPaneProps => { return { propertySections: getPropertyConfig(state), + dataTree: getDataTree(state), + propertyErrors: getPropertyErrors(state), widgetId: getCurrentWidgetId(state), widgetProperties: getCurrentWidgetProperties(state), isVisible: getIsPropertyPaneVisible(state), @@ -184,11 +231,21 @@ const mapDispatchToProps = (dispatch: any): PropertyPaneFunctions => { dispatch({ type: ReduxActionTypes.HIDE_PROPERTY_PANE, }), + setPropertyValidation: ( + widgetId: string, + propertyName: string, + errorCode: ErrorCode, + ) => + dispatch( + updateWidgetPropertyValidation(widgetId, propertyName, errorCode), + ), }; }; export interface PropertyPaneProps { propertySections?: PropertySection[]; + propertyErrors: Record>; + dataTree: DataTree; widgetId?: string; widgetProperties?: any; //TODO(abhinav): Secure type definition isVisible: boolean; @@ -198,6 +255,11 @@ export interface PropertyPaneProps { export interface PropertyPaneFunctions { updateWidgetProperty: Function; hidePropertyPane: () => void; + setPropertyValidation: ( + widgetId: string, + propertyName: string, + errorCode: ErrorCode, + ) => void; } export default connect( diff --git a/app/client/src/reducers/entityReducers/apiDataReducer.tsx b/app/client/src/reducers/entityReducers/apiDataReducer.tsx index 24d1b4f05e..797fd1df6b 100644 --- a/app/client/src/reducers/entityReducers/apiDataReducer.tsx +++ b/app/client/src/reducers/entityReducers/apiDataReducer.tsx @@ -8,9 +8,7 @@ import { ActionDataState } from "./actionsReducer"; const initialState: APIDataState = {}; -export interface APIDataState { - [id: string]: ActionResponse; -} +export type APIDataState = Record; const apiDataReducer = createReducer(initialState, { [ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: ( diff --git a/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx b/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx index 58d73b3256..ed81850e54 100644 --- a/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx +++ b/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx @@ -1,14 +1,18 @@ +import _ from "lodash"; import { createReducer } from "../../utils/AppsmithUtils"; import { ReduxActionTypes, ReduxAction, ShowPropertyPanePayload, } from "../../constants/ReduxActionConstants"; +import { ERROR_CODES, ErrorCode } from "../../constants/validationErrorCodes"; +import { UpdateWidgetPropertyValidation } from "../../actions/controlActions"; const initialState: PropertyPaneReduxState = { isVisible: false, widgetId: undefined, node: undefined, + errors: {}, }; const propertyPaneReducer = createReducer(initialState, { @@ -24,17 +28,32 @@ const propertyPaneReducer = createReducer(initialState, { if (toggle) { isVisible = !state.isVisible; } - return { widgetId, node, isVisible }; + return { ...state, widgetId, node, isVisible }; }, [ReduxActionTypes.HIDE_PROPERTY_PANE]: (state: PropertyPaneReduxState) => { return { ...state, isVisible: false }; }, + [ReduxActionTypes.UPDATE_WIDGET_PROPERTY_VALIDATION]: ( + state: PropertyPaneReduxState, + action: ReduxAction, + ) => { + const { widgetId, propertyName, errorCode } = action.payload; + let widgetErrors = { ...state.errors[widgetId] }; + if (action.payload.errorCode === ERROR_CODES.NO_ERROR) { + widgetErrors = _.omit(widgetErrors, propertyName); + } else { + widgetErrors[propertyName] = errorCode; + } + const errors = { ...state.errors, [widgetId]: widgetErrors }; + return { ...state, errors }; + }, }); export interface PropertyPaneReduxState { widgetId?: string; isVisible: boolean; node?: HTMLDivElement; + errors: Record>; } export default propertyPaneReducer; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 16c15a801d..e9dae414fb 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -20,7 +20,7 @@ import ActionAPI, { ExecuteActionRequest, RestAction, } from "../api/ActionAPI"; -import { AppState, DataTree } from "../reducers"; +import { AppState } from "../reducers"; import _ from "lodash"; import { mapToPropList } from "../utils/AppsmithUtils"; import AppToaster from "../components/editorComponents/ToastComponent"; @@ -31,18 +31,15 @@ import { updateActionSuccess, } from "../actions/actionActions"; import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../constants/routes"; -import { getDynamicBoundValue } from "../utils/DynamicBindingUtils"; +import { extractDynamicBoundValue } from "../utils/DynamicBindingUtils"; import history from "../utils/history"; import { validateResponse } from "./ErrorSagas"; +import { getDataTree } from "../selectors/entitiesSelector"; import { ERROR_MESSAGE_SELECT_ACTION, ERROR_MESSAGE_SELECT_ACTION_TYPE, } from "constants/messages"; -const getDataTree = (state: AppState): DataTree => { - return state.entities; -}; - const getAction = ( state: AppState, actionId: string, @@ -69,7 +66,7 @@ const createActionErrorResponse = ( export function* evaluateJSONPathSaga(path: string): any { const dataTree = yield select(getDataTree); - return getDynamicBoundValue(dataTree, path); + return extractDynamicBoundValue(dataTree, path); } export function* executeAPIQueryActionSaga(apiAction: ActionPayload) { diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 826f97460c..e770d99518 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -18,7 +18,7 @@ import { import { put, select, takeEvery, takeLatest, all } from "redux-saga/effects"; import { getNextWidgetName } from "../utils/AppsmithUtils"; import { UpdateWidgetPropertyPayload } from "../actions/controlActions"; -import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; +import { isDynamicValue } from "../utils/DynamicBindingUtils"; export function* addChildSaga(addChildAction: ReduxAction) { try { @@ -173,8 +173,7 @@ function* updateWidgetPropertySaga( payload: { propertyValue }, } = updateAction; - const isDynamic = DATA_BIND_REGEX.test(propertyValue); - if (isDynamic) { + if (isDynamicValue(propertyValue)) { yield put({ type: ReduxActionTypes.UPDATE_WIDGET_DYNAMIC_PROPERTY, payload: updateAction.payload, diff --git a/app/client/src/selectors/appViewSelectors.tsx b/app/client/src/selectors/appViewSelectors.tsx index 3eeb2eedbd..549e58745c 100644 --- a/app/client/src/selectors/appViewSelectors.tsx +++ b/app/client/src/selectors/appViewSelectors.tsx @@ -3,9 +3,9 @@ import { AppState, DataTree } from "../reducers"; import { AppViewReduxState } from "../reducers/uiReducers/appViewReducer"; import { AppViewerProps } from "../pages/AppViewer"; import { injectDataTreeIntoDsl } from "../utils/DynamicBindingUtils"; +import { getDataTree } from "./entitiesSelector"; const getAppViewState = (state: AppState) => state.ui.appView; -const getDataTree = (state: AppState): DataTree => state.entities; export const getCurrentLayoutId = (state: AppState, props: AppViewerProps) => state.ui.appView.currentLayoutId || props.match.params.layoutId; diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 1beb915791..3c20ff3b52 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -8,6 +8,7 @@ import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import { injectDataTreeIntoDsl } from "utils/DynamicBindingUtils"; +import { getDataTree } from "./entitiesSelector"; import { FlattenedWidgetProps, CanvasWidgetsReduxState, @@ -18,7 +19,6 @@ import { WidgetTypes } from "constants/WidgetConstants"; const getEditorState = (state: AppState) => state.ui.editor; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; -const getEntities = (state: AppState) => state.entities; const getWidgetSideBar = (state: AppState) => state.ui.widgetSidebar; const getWidgets = (state: AppState): CanvasWidgetsReduxState => @@ -102,7 +102,7 @@ export const getWidgetCards = createSelector( export const getDenormalizedDSL = createCachedSelector( getPageWidgetId, - getEntities, + getDataTree, (pageWidgetId: string, entities: DataTree) => { const dsl = CanvasWidgetsNormalizer.denormalize(pageWidgetId, entities); return injectDataTreeIntoDsl(entities, dsl); diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts new file mode 100644 index 0000000000..1ee868eb19 --- /dev/null +++ b/app/client/src/selectors/entitiesSelector.ts @@ -0,0 +1,3 @@ +import { AppState, DataTree } from "../reducers"; + +export const getDataTree = (state: AppState): DataTree => state.entities; diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index c951771101..f750efbb35 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -61,3 +61,8 @@ export const getIsPropertyPaneVisible = createSelector( (pane: PropertyPaneReduxState, content?: PropertySection[]) => !!(pane.isVisible && pane.widgetId && pane.node && content), ); + +export const getPropertyErrors = createSelector( + getPropertyPaneState, + (pane: PropertyPaneReduxState) => pane.errors || {}, +); diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index e25b3b7a83..7be320263a 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -8,11 +8,28 @@ import { DATA_PATH_REGEX, } from "../constants/BindingsConstants"; +export const isDynamicValue = (value: string): boolean => + DATA_BIND_REGEX.test(value); + +export const getDynamicBindings = ( + dynamicString: string, +): { bindings: string[]; paths: string[] } => { + // Get the {{binding}} bound values + const bindings = dynamicString.match(DATA_BIND_REGEX) || []; + // Get the "binding" path values + const paths = bindings.map(p => { + const matches = p.match(DATA_PATH_REGEX); + if (matches) return matches[0]; + return ""; + }); + return { bindings, paths }; +}; + // Paths are expected to have "{name}.{path}" signature -export const getDynamicBoundValue = ( +export const extractDynamicBoundValue = ( dataTree: DataTree, path: string, -): Array => { +): any => { // Remove the name in the binding const splitPath = path.split("."); // Find the dataTree path of the name @@ -20,7 +37,42 @@ export const getDynamicBoundValue = ( // Create the full path const fullPath = `${bindingPath}.${splitPath.slice(1).join(".")}`; // Search with JSONPath - return JSONPath({ path: fullPath, json: dataTree }); + return JSONPath({ path: fullPath, json: dataTree })[0]; +}; + +// For creating a final value where bindings could be in a template format +export const createDynamicValueString = ( + binding: string, + subBindings: string[], + subValues: string[], +): string => { + // Replace the string with the data tree values + let finalValue = binding; + subBindings.forEach((b, i) => { + let value = subValues[i]; + if (Array.isArray(value) || _.isObject(value)) { + value = JSON.stringify(value); + } + finalValue = finalValue.replace(b, value); + }); + return finalValue; +}; + +export const getDynamicValue = ( + dynamicBinding: string, + dataTree: DataTree, +): any => { + // Get the {{binding}} bound values + const { bindings, paths } = getDynamicBindings(dynamicBinding); + if (bindings.length) { + // Get the Data Tree value of those "binding "paths + const values = paths.map(p => extractDynamicBoundValue(dataTree, p)); + // if it is just one binding, no need to create template string + if (bindings.length === 1) return values[0]; + // else return a string template with bindings + return createDynamicValueString(dynamicBinding, bindings, values); + } + return undefined; }; export const injectDataTreeIntoDsl = ( @@ -36,33 +88,7 @@ export const injectDataTreeIntoDsl = ( // Check for dynamic bindings if (dynamicBindings && !_.isEmpty(dynamicBindings)) { Object.keys(dynamicBindings).forEach((dKey: string) => { - // Get the {{binding}} bound values - const bindings = dynamicBindings[dKey].match(DATA_BIND_REGEX); - if (bindings && bindings.length) { - // Get the "binding" path values - const paths = bindings.map(p => { - const matches = p.match(DATA_PATH_REGEX); - if (matches) return matches[0]; - return ""; - }); - // Get the Data Tree value of those "binding "paths - const values = paths.map(p => { - const value = getDynamicBoundValue(entities, p)[0]; - if (value) return value; - return "undefined"; - }); - // Replace the string with the data tree values - let string = dynamicBindings[dKey]; - bindings.forEach((b, i) => { - let value = values[i]; - if (Array.isArray(value)) { - value = JSON.stringify(value); - } - string = string.replace(b, value); - }); - // Overwrite the property with the evaluated data tree property - widget[dKey] = string; - } + widget[dKey] = getDynamicValue(dynamicBindings[dKey], entities); }); } if (tree.children) { diff --git a/app/client/src/utils/DynamicBindingsUtil.test.ts b/app/client/src/utils/DynamicBindingsUtil.test.ts new file mode 100644 index 0000000000..84a57c8a3d --- /dev/null +++ b/app/client/src/utils/DynamicBindingsUtil.test.ts @@ -0,0 +1,34 @@ +import { getDynamicValue } from "./DynamicBindingUtils"; +import { DataTree } from "../reducers"; + +it("Gets the value from the data tree", () => { + const dynamicBinding = "{{GetUsers.data}}"; + const dataTree: Partial = { + apiData: { + id: { + body: { + data: "correct data", + }, + headers: {}, + statusCode: "0", + duration: "0", + size: "0", + }, + someOtherId: { + body: { + data: "wrong data", + }, + headers: {}, + statusCode: "0", + duration: "0", + size: "0", + }, + }, + nameBindings: { + GetUsers: "$.apiData.id.body", + }, + }; + const actualValue = "correct data"; + const value = getDynamicValue(dynamicBinding, dataTree); + expect(value).toEqual(actualValue); +}); diff --git a/app/client/src/widgets/ContainerWidget.tsx b/app/client/src/widgets/ContainerWidget.tsx index 8182d13939..9fb6b25d27 100644 --- a/app/client/src/widgets/ContainerWidget.tsx +++ b/app/client/src/widgets/ContainerWidget.tsx @@ -1,7 +1,7 @@ import React from "react"; import _ from "lodash"; -import ContainerComponent from "components/designSystems/appsmith/ContainerComponent"; +import ContainerComponent from "../components/designSystems/appsmith/ContainerComponent"; import { ContainerOrientation, WidgetType } from "constants/WidgetConstants"; import WidgetFactory from "utils/WidgetFactory"; import { Color } from "constants/Colors"; diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index b6f339c737..97e5ad8d10 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -1,4 +1,5 @@ import React from "react"; +import _ from "lodash"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "../constants/WidgetConstants"; import { ActionPayload } from "../constants/ActionConstants"; @@ -26,27 +27,24 @@ function constructColumns(data: object[]): Column[] { return cols; } -function parseTableArray(parsable: string): object[] { - let data: object[] = []; +function getTableArrayData(tableData: string | object[] | undefined): object[] { try { - const parsedData = JSON.parse(parsable); - if (!Array.isArray(parsedData)) { - throw new Error("Parsed Data is an object"); + if (!tableData) return []; + if (_.isString(tableData)) { + return JSON.parse(tableData); } - data = parsedData; - } catch (ex) { - console.log(ex); + return tableData; + } catch (error) { + console.error({ error }); + return []; } - return data; } class TableWidget extends BaseWidget { getPageView() { - const tableData = parseTableArray( - this.props.tableData ? ((this.props.tableData as any) as string) : "", - ); - - const columns = constructColumns(tableData); + const { tableData } = this.props; + const data = getTableArrayData(tableData); + const columns = constructColumns(data); return ( {({ width, height }: { width: number; height: number }) => ( @@ -54,7 +52,7 @@ class TableWidget extends BaseWidget { width={width} height={height} columns={columns} - data={tableData} + data={data} maxHeight={height} selectedRowIndex={ this.props.selectedRow && this.props.selectedRow.rowIndex diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json index 1ef084c2fe..6640dc9f3e 100644 --- a/app/client/tsconfig.json +++ b/app/client/tsconfig.json @@ -26,7 +26,7 @@ "importHelpers": true, "typeRoots" : ["./typings", "./node_modules/@types"], "sourceMap": true, - "baseUrl": "./src", + "baseUrl": "./src" }, "include": [ "./src/**/*" diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 64eb8f8cbc..d6e6d70c3a 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -1607,6 +1607,18 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest-diff@*": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" + integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + +"@types/jest@^24.0.22": + version "24.0.22" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.22.tgz#08a50be08e78aba850a1185626e71d31e2336145" + integrity sha512-t2OvhNZnrNjlzi2i0/cxbLVM59WN15I2r1Qtb7wDv28PnV9IzrPtagFRey/S9ezdLD0zyh1XGMQIEQND2YEfrw== + dependencies: + "@types/jest-diff" "*" + "@types/json-schema@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" @@ -10086,7 +10098,7 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.4: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.4, react-is@^16.8.6: version "16.11.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== @@ -10249,6 +10261,16 @@ react-select@^3.0.8: react-input-autosize "^2.2.2" react-transition-group "^2.2.1" +react-test-renderer@^16.11.0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8" + integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.17.0" + react-transition-group@^2.2.1, react-transition-group@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"