Merge branch 'feature/widget-property-parsing' into 'release'

Validation parse widget property

Closes: #298 
* Added functionality in validators to provide a parsed value to be used by widgets
* Added validation to (almost) all properties of the widgets

See merge request theappsmith/internal-tools-client!160
This commit is contained in:
Hetu Nandu 2019-11-22 13:12:39 +00:00
commit a55ff37ebd
19 changed files with 248 additions and 74 deletions

View File

@ -44,6 +44,7 @@
"jsonpath-plus": "^1.0.0",
"lint-staged": "^9.2.5",
"lodash": "^4.17.11",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
"monaco-editor": "^0.15.1",
"monaco-editor-webpack-plugin": "^1.7.0",

View File

@ -36,10 +36,7 @@ class InputTextControl extends BaseControl<InputControlProps> {
}
onTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let value: string | number = event.target.value;
if (this.isNumberType()) {
value = _.toNumber(value);
}
const value: string = event.target.value;
this.updateProperty(this.props.propertyName, value);
};

View File

@ -1,10 +1,18 @@
// Always add a validator function in ./Validators for these types
export const VALIDATION_TYPES = {
TEXT: "TEXT",
NUMBER: "NUMBER",
BOOLEAN: "BOOLEAN",
OBJECT: "OBJECT",
ARRAY: "ARRAY",
TABLE_DATA: "TABLE_DATA",
DATE: "DATE",
};
export type ValidationResponse = {
isValid: boolean;
parsed: any;
};
export type ValidationType = (typeof VALIDATION_TYPES)[keyof typeof VALIDATION_TYPES];
export type Validator = (value: any) => boolean;
export type Validator = (value: any) => ValidationResponse;

View File

@ -124,5 +124,8 @@ const mapDispatchToProps = (dispatch: any) => ({
});
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Applications),
connect(
mapStateToProps,
mapDispatchToProps,
)(Applications),
);

View File

@ -78,7 +78,7 @@ export const getDynamicValue = (
export const enhanceWithDynamicValuesAndValidations = (
widget: WidgetProps,
entities: DataTree,
safeValues: boolean,
replaceWithParsed: boolean,
): WidgetProps => {
if (!widget) return widget;
const properties = { ...widget };
@ -86,19 +86,19 @@ export const enhanceWithDynamicValuesAndValidations = (
Object.keys(widget).forEach((property: string) => {
let value = widget[property];
// Check for dynamic bindings
if (isDynamicValue(value)) {
if (widget.dynamicBindings && property in widget.dynamicBindings) {
value = getDynamicValue(value, entities);
}
const isValid = ValidationFactory.validateWidgetProperty(
// Pass it through validation and parse
const { isValid, parsed } = ValidationFactory.validateWidgetProperty(
widget.type,
property,
value,
);
if (!isValid) {
if (safeValues) value = undefined;
invalidProps[property] = true;
}
if (safeValues) properties[property] = value;
// Store all invalid props
if (!isValid) invalidProps[property] = true;
// Replace if flag is turned on
if (replaceWithParsed) properties[property] = parsed;
});
return { ...properties, invalidProps };
};

View File

@ -1,6 +1,10 @@
import { WidgetType } from "constants/WidgetConstants";
import WidgetFactory from "./WidgetFactory";
import { ValidationType, Validator } from "../constants/WidgetValidation";
import {
ValidationResponse,
ValidationType,
Validator,
} from "../constants/WidgetValidation";
// TODO: need to be strict about what the key can be
export type WidgetPropertyValidationType = Record<string, ValidationType>;
@ -19,17 +23,17 @@ class ValidationFactory {
widgetType: WidgetType,
property: string,
value: any,
) {
let isValid = true;
): ValidationResponse {
const propertyValidationTypes = WidgetFactory.getWidgetPropertyValidationMap(
widgetType,
);
const validationType = propertyValidationTypes[property];
const validator = this.validationMap.get(validationType);
if (validator) {
isValid = validator(value);
return validator(value);
} else {
return { isValid: true, parsed: value };
}
return isValid;
}
}

View File

@ -4,30 +4,9 @@ import { VALIDATORS } from "./Validators";
class ValidationRegistry {
static registerInternalValidators() {
ValidationFactory.registerValidator(
VALIDATION_TYPES.TEXT,
VALIDATORS[VALIDATION_TYPES.TEXT],
);
ValidationFactory.registerValidator(
VALIDATION_TYPES.NUMBER,
VALIDATORS[VALIDATION_TYPES.NUMBER],
);
ValidationFactory.registerValidator(
VALIDATION_TYPES.BOOLEAN,
VALIDATORS[VALIDATION_TYPES.BOOLEAN],
);
ValidationFactory.registerValidator(
VALIDATION_TYPES.OBJECT,
VALIDATORS[VALIDATION_TYPES.OBJECT],
);
ValidationFactory.registerValidator(
VALIDATION_TYPES.TABLE_DATA,
VALIDATORS[VALIDATION_TYPES.TABLE_DATA],
);
Object.keys(VALIDATION_TYPES).forEach(type => {
ValidationFactory.registerValidator(type, VALIDATORS[type]);
});
}
}

View File

@ -1,25 +1,115 @@
import _ from "lodash";
import {
VALIDATION_TYPES,
ValidationResponse,
ValidationType,
Validator,
} from "../constants/WidgetValidation";
import moment from "moment";
export const VALIDATORS: Record<ValidationType, Validator> = {
[VALIDATION_TYPES.TEXT]: (value: any) => _.isString(value),
[VALIDATION_TYPES.NUMBER]: (value: any) => _.isNumber(value),
[VALIDATION_TYPES.BOOLEAN]: (value: any) => _.isBoolean(value),
[VALIDATION_TYPES.OBJECT]: (value: any) => _.isObject(value),
[VALIDATION_TYPES.TABLE_DATA]: (value: any) => {
try {
let data = value;
if (_.isString(data)) {
data = JSON.parse(data as string);
[VALIDATION_TYPES.TEXT]: (value: any): ValidationResponse => {
let parsed = value;
if (_.isUndefined(value) || _.isObject(value)) {
return { isValid: false, parsed: "" };
}
let isValid = _.isString(value);
if (!isValid) {
try {
parsed = _.toString(value);
isValid = true;
} catch (e) {
console.error(`Error when parsing ${value} to string`);
console.error(e);
return { isValid: false, parsed: "" };
}
if (!Array.isArray(data)) return false;
return _.every(data, datum => _.isObject(datum));
} catch {
return false;
}
return { isValid, parsed };
},
[VALIDATION_TYPES.NUMBER]: (value: any): ValidationResponse => {
let parsed = value;
if (_.isUndefined(value)) {
return { isValid: false, parsed: 0 };
}
let isValid = _.isNumber(value);
if (!isValid) {
try {
parsed = _.toNumber(value);
isValid = true;
} catch (e) {
console.error(`Error when parsing ${value} to number`);
console.error(e);
return { isValid: false, parsed: 0 };
}
}
return { isValid, parsed };
},
[VALIDATION_TYPES.BOOLEAN]: (value: any): ValidationResponse => {
let parsed = value;
if (_.isUndefined(value)) {
return { isValid: false, parsed: false };
}
let isValid = _.isBoolean(value);
if (!isValid) {
try {
parsed = !!value;
isValid = true;
} catch (e) {
console.error(`Error when parsing ${value} to boolean`);
console.error(e);
return { isValid: false, parsed: false };
}
}
return { isValid, parsed };
},
[VALIDATION_TYPES.OBJECT]: (value: any): ValidationResponse => {
let parsed = value;
if (_.isUndefined(value)) {
return { isValid: false, parsed: {} };
}
let isValid = _.isObject(value);
if (!isValid) {
try {
parsed = JSON.parse(value);
isValid = true;
} catch (e) {
console.error(`Error when parsing ${value} to object`);
console.error(e);
return { isValid: false, parsed: {} };
}
}
return { isValid, parsed };
},
[VALIDATION_TYPES.ARRAY]: (value: any): ValidationResponse => {
let parsed = value;
try {
if (_.isUndefined(value)) {
return { isValid: false, parsed: [] };
}
if (_.isString(value)) {
parsed = JSON.parse(parsed as string);
}
if (!Array.isArray(parsed)) {
return { isValid: false, parsed: [] };
}
return { isValid: true, parsed };
} catch (e) {
console.error(e);
return { isValid: false, parsed: [] };
}
},
[VALIDATION_TYPES.TABLE_DATA]: (value: any): ValidationResponse => {
const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY](value);
if (!isValid) {
return { isValid, parsed };
} else if (!_.every(parsed, datum => _.isObject(datum))) {
return { isValid: false, parsed: [] };
}
return { isValid, parsed };
},
[VALIDATION_TYPES.DATE]: (value: any): ValidationResponse => {
const isValid = moment(value).isValid();
const parsed = isValid ? moment(value).toDate() : new Date();
return { isValid, parsed };
},
};

View File

@ -3,6 +3,8 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import ButtonComponent from "../components/designSystems/blueprint/ButtonComponent";
import { ActionPayload } from "../constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
@ -12,6 +14,15 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
this.onButtonClickBound = this.onButtonClick.bind(this);
}
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
text: VALIDATION_TYPES.TEXT,
isDisabled: VALIDATION_TYPES.BOOLEAN,
isVisible: VALIDATION_TYPES.BOOLEAN,
buttonStyle: VALIDATION_TYPES.TEXT,
};
}
onButtonClick() {
super.executeAction(this.props.onClick);
}

View File

@ -11,6 +11,8 @@ class CheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
return {
isDisabled: VALIDATION_TYPES.BOOLEAN,
label: VALIDATION_TYPES.TEXT,
defaultCheckedState: VALIDATION_TYPES.BOOLEAN,
isChecked: VALIDATION_TYPES.BOOLEAN,
};
}

View File

@ -3,8 +3,23 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import { ActionPayload } from "../constants/ActionConstants";
import DatePickerComponent from "../components/designSystems/blueprint/DatePickerComponent";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class DatePickerWidget extends BaseWidget<DatePickerWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
defaultDate: VALIDATION_TYPES.DATE,
selectedDate: VALIDATION_TYPES.DATE,
timezone: VALIDATION_TYPES.TEXT,
enableTimePicker: VALIDATION_TYPES.BOOLEAN,
dateFormat: VALIDATION_TYPES.TEXT,
label: VALIDATION_TYPES.TEXT,
datePickerType: VALIDATION_TYPES.TEXT,
maxDate: VALIDATION_TYPES.DATE,
minDate: VALIDATION_TYPES.DATE,
};
}
getPageView() {
return (
<DatePickerComponent

View File

@ -4,8 +4,20 @@ import { WidgetType } from "../constants/WidgetConstants";
import { ActionPayload } from "../constants/ActionConstants";
import DropDownComponent from "../components/designSystems/blueprint/DropdownComponent";
import _ from "lodash";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
placeholderText: VALIDATION_TYPES.TEXT,
label: VALIDATION_TYPES.TEXT,
options: VALIDATION_TYPES.ARRAY,
selectionType: VALIDATION_TYPES.TEXT,
selectedIndex: VALIDATION_TYPES.NUMBER,
selectedIndexArr: VALIDATION_TYPES.ARRAY,
};
}
getPageView() {
return (
<DropDownComponent

View File

@ -7,6 +7,8 @@ import Webcam from "@uppy/webcam";
import Url from "@uppy/url";
import OneDrive from "@uppy/onedrive";
import FilePickerComponent from "../components/designSystems/appsmith/FilePickerComponent";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class FilePickerWidget extends BaseWidget<FilePickerWidgetProps, WidgetState> {
uppy: any;
@ -16,6 +18,14 @@ class FilePickerWidget extends BaseWidget<FilePickerWidgetProps, WidgetState> {
this.refreshUppy(props);
}
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
label: VALIDATION_TYPES.TEXT,
maxNumFiles: VALIDATION_TYPES.NUMBER,
allowedFileTypes: VALIDATION_TYPES.ARRAY,
};
}
refreshUppy = (props: FilePickerWidgetProps) => {
this.uppy = Uppy({
id: this.props.widgetId,

View File

@ -2,8 +2,17 @@ import * as React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import ImageComponent from "../components/designSystems/appsmith/ImageComponent";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class ImageWidget extends BaseWidget<ImageWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
image: VALIDATION_TYPES.TEXT,
imageShape: VALIDATION_TYPES.TEXT,
defaultImage: VALIDATION_TYPES.TEXT,
};
}
getPageView() {
return (
<ImageComponent

View File

@ -3,8 +3,28 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import InputComponent from "components/designSystems/blueprint/InputComponent";
import { ActionPayload } from "constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
inputType: VALIDATION_TYPES.TEXT,
defaultText: VALIDATION_TYPES.TEXT,
isDisabled: VALIDATION_TYPES.BOOLEAN,
text: VALIDATION_TYPES.TEXT,
regex: VALIDATION_TYPES.TEXT,
errorMessage: VALIDATION_TYPES.TEXT,
placeholderText: VALIDATION_TYPES.TEXT,
maxChars: VALIDATION_TYPES.NUMBER,
minNum: VALIDATION_TYPES.NUMBER,
maxNum: VALIDATION_TYPES.NUMBER,
label: VALIDATION_TYPES.TEXT,
inputValidators: VALIDATION_TYPES.ARRAY,
focusIndex: VALIDATION_TYPES.NUMBER,
isAutoFocusEnabled: VALIDATION_TYPES.BOOLEAN,
};
}
regex = new RegExp("");
componentDidMount() {

View File

@ -3,8 +3,17 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import RadioGroupComponent from "../components/designSystems/blueprint/RadioGroupComponent";
import { ActionPayload } from "../constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class RadioGroupWidget extends BaseWidget<RadioGroupWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
label: VALIDATION_TYPES.TEXT,
options: VALIDATION_TYPES.ARRAY,
selectedOptionValue: VALIDATION_TYPES.TEXT,
};
}
getPageView() {
return (
<RadioGroupComponent

View File

@ -3,8 +3,17 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import { Intent } from "@blueprintjs/core";
import SpinnerComponent from "../components/designSystems/blueprint/SpinnerComponent";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class SpinnerWidget extends BaseWidget<SpinnerWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
size: VALIDATION_TYPES.NUMBER,
value: VALIDATION_TYPES.NUMBER,
ellipsize: VALIDATION_TYPES.BOOLEAN,
};
}
getPageView() {
return (
<SpinnerComponent

View File

@ -29,28 +29,20 @@ function constructColumns(data: object[]): Column[] {
return cols;
}
const getTableArrayData = (
tableData: string | object[] | undefined,
): object[] => {
if (!tableData) return [];
if (_.isString(tableData)) {
return JSON.parse(tableData as string);
} else {
return tableData;
}
};
class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
tableData: VALIDATION_TYPES.TABLE_DATA,
nextPageKey: VALIDATION_TYPES.TEXT,
prevPageKey: VALIDATION_TYPES.TEXT,
label: VALIDATION_TYPES.TEXT,
selectedRow: VALIDATION_TYPES.OBJECT,
};
}
getPageView() {
const { tableData } = this.props;
const data = getTableArrayData(tableData);
const columns = constructColumns(data);
const columns = constructColumns(tableData);
return (
<AutoResizer>
{({ width, height }: { width: number; height: number }) => (
@ -58,7 +50,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
width={width}
height={height}
columns={columns}
data={data}
data={tableData}
maxHeight={height}
isLoading={this.props.isLoading}
selectedRowIndex={
@ -76,10 +68,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
}
componentDidUpdate(prevProps: TableWidgetProps) {
super.componentDidUpdate(prevProps);
const newData = getTableArrayData(this.props.tableData);
if (prevProps.tableData !== this.props.tableData && prevProps.selectedRow) {
if (
!_.isEqual(prevProps.tableData, this.props.tableData) &&
prevProps.selectedRow
) {
this.updateSelectedRowProperty(
newData[prevProps.selectedRow.rowIndex],
this.props.tableData[prevProps.selectedRow.rowIndex],
prevProps.selectedRow.rowIndex,
);
}
@ -113,7 +107,7 @@ export interface TableWidgetProps extends WidgetProps {
nextPageKey?: string;
prevPageKey?: string;
label: string;
tableData?: string | object[];
tableData: object[];
recordActions?: TableAction[];
onPageChange?: ActionPayload[];
onRowSelected?: ActionPayload[];

View File

@ -9,6 +9,7 @@ class TextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
text: VALIDATION_TYPES.TEXT,
textStyle: VALIDATION_TYPES.TEXT,
};
}