PromucFlow_constructor/app/client/src/widgets/InputWidgetV2/widget/index.tsx

492 lines
14 KiB
TypeScript

import React from "react";
import { WidgetState } from "widgets/BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import InputComponent, { InputComponentProps } from "../component";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import {
ValidationTypes,
ValidationResponse,
} from "constants/WidgetValidation";
import {
createMessage,
FIELD_REQUIRED_ERROR,
INPUT_DEFAULT_TEXT_MAX_CHAR_ERROR,
} from "@appsmith/constants/messages";
import { DerivedPropertiesMap } from "utils/WidgetFactory";
import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
import { AutocompleteDataType } from "utils/autocomplete/TernServer";
import BaseInputWidget from "widgets/BaseInputWidget";
import _, { isNil } from "lodash";
import derivedProperties from "./parsedDerivedProperties";
import { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget";
import { mergeWidgetConfig } from "utils/helpers";
import { InputTypes } from "widgets/BaseInputWidget/constants";
export function defaultValueValidation(
value: any,
props: InputWidgetProps,
_?: any,
): ValidationResponse {
const STRING_ERROR_MESSAGE = "This value must be string";
const NUMBER_ERROR_MESSAGE = "This value must be number";
const EMPTY_ERROR_MESSAGE = "";
if (_.isObject(value)) {
return {
isValid: false,
parsed: JSON.stringify(value, null, 2),
messages: [STRING_ERROR_MESSAGE],
};
}
const { inputType } = props;
let parsed;
switch (inputType) {
case "NUMBER":
parsed = Number(value);
let isValid, messages;
if (_.isString(value) && value.trim() === "") {
/*
* When value is emtpy string
*/
isValid = true;
messages = [EMPTY_ERROR_MESSAGE];
parsed = undefined;
} else if (!Number.isFinite(parsed)) {
/*
* When parsed value is not a finite numer
*/
isValid = false;
messages = [NUMBER_ERROR_MESSAGE];
parsed = undefined;
} else {
/*
* When parsed value is a Number
*/
isValid = true;
messages = [EMPTY_ERROR_MESSAGE];
}
return {
isValid,
parsed,
messages,
};
case "TEXT":
case "PASSWORD":
case "EMAIL":
parsed = value;
if (!_.isString(parsed)) {
try {
parsed = _.toString(parsed);
} catch (e) {
return {
isValid: false,
parsed: "",
messages: [STRING_ERROR_MESSAGE],
};
}
}
return {
isValid: _.isString(parsed),
parsed: parsed,
messages: [EMPTY_ERROR_MESSAGE],
};
default:
return {
isValid: false,
parsed: "",
messages: [STRING_ERROR_MESSAGE],
};
}
}
export function minValueValidation(min: any, props: InputWidgetProps, _?: any) {
const max = props.maxNum;
const value = min;
min = Number(min);
if (_?.isNil(value) || value === "") {
return {
isValid: true,
parsed: undefined,
messages: [""],
};
} else if (!Number.isFinite(min)) {
return {
isValid: false,
parsed: undefined,
messages: ["This value must be number"],
};
} else if (max !== undefined && min >= max) {
return {
isValid: false,
parsed: undefined,
messages: ["This value must be lesser than max value"],
};
} else {
return {
isValid: true,
parsed: Number(min),
messages: [""],
};
}
}
export function maxValueValidation(max: any, props: InputWidgetProps, _?: any) {
const min = props.minNum;
const value = max;
max = Number(max);
if (_?.isNil(value) || value === "") {
return {
isValid: true,
parsed: undefined,
messages: [""],
};
} else if (!Number.isFinite(max)) {
return {
isValid: false,
parsed: undefined,
messages: ["This value must be number"],
};
} else if (min !== undefined && max <= min) {
return {
isValid: false,
parsed: undefined,
messages: ["This value must be greater than min value"],
};
} else {
return {
isValid: true,
parsed: Number(max),
messages: [""],
};
}
}
class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
static getPropertyPaneConfig() {
return mergeWidgetConfig(
[
{
sectionName: "General",
children: [
{
helpText: "Changes the type of data captured in the input",
propertyName: "inputType",
label: "Data Type",
controlType: "DROP_DOWN",
options: [
{
label: "Text",
value: "TEXT",
},
{
label: "Number",
value: "NUMBER",
},
{
label: "Password",
value: "PASSWORD",
},
{
label: "Email",
value: "EMAIL",
},
],
isBindProperty: false,
isTriggerProperty: false,
},
{
helpText: "Sets maximum allowed text length",
propertyName: "maxChars",
label: "Max Chars",
controlType: "INPUT_TEXT",
placeholderText: "255",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.NUMBER,
params: { min: 1, natural: true },
},
hidden: (props: InputWidgetProps) => {
return props.inputType !== InputTypes.TEXT;
},
dependencies: ["inputType"],
},
{
helpText:
"Sets the default text of the widget. The text is updated if the default text changes",
propertyName: "defaultText",
label: "Default Text",
controlType: "INPUT_TEXT",
placeholderText: "John Doe",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.FUNCTION,
params: {
fn: defaultValueValidation,
expected: {
type: "string or number",
example: `John | 123`,
autocompleteDataType: AutocompleteDataType.STRING,
},
},
},
dependencies: ["inputType"],
},
{
helpText: "Sets the minimum allowed value",
propertyName: "minNum",
label: "Min",
controlType: "INPUT_TEXT",
placeholderText: "1",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.FUNCTION,
params: {
fn: minValueValidation,
expected: {
type: "number",
example: `1`,
autocompleteDataType: AutocompleteDataType.NUMBER,
},
},
},
hidden: (props: InputWidgetProps) => {
return props.inputType !== InputTypes.NUMBER;
},
dependencies: ["inputType"],
},
{
helpText: "Sets the maximum allowed value",
propertyName: "maxNum",
label: "Max",
controlType: "INPUT_TEXT",
placeholderText: "100",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.FUNCTION,
params: {
fn: maxValueValidation,
expected: {
type: "number",
example: `100`,
autocompleteDataType: AutocompleteDataType.NUMBER,
},
},
},
hidden: (props: InputWidgetProps) => {
return props.inputType !== InputTypes.NUMBER;
},
dependencies: ["inputType"],
},
],
},
{
sectionName: "Icon Options",
children: [
{
propertyName: "iconName",
label: "Icon",
helpText: "Sets the icon to be used in input field",
controlType: "ICON_SELECT",
isBindProperty: false,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "iconAlign",
label: "Icon alignment",
helpText: "Sets the icon alignment of input field",
controlType: "ICON_TABS",
options: [
{
icon: "VERTICAL_LEFT",
value: "left",
},
{
icon: "VERTICAL_RIGHT",
value: "right",
},
],
isBindProperty: false,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
hidden: (props: InputWidgetProps) => !props.iconName,
dependencies: ["iconName"],
},
],
},
],
super.getPropertyPaneConfig(),
);
}
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return _.merge(super.getDerivedPropertiesMap(), {
isValid: `{{(() => {${derivedProperties.isValid}})()}}`,
});
}
static getMetaPropertiesMap(): Record<string, any> {
return super.getMetaPropertiesMap();
}
handleFocusChange = (focusState: boolean) => {
super.handleFocusChange(focusState);
};
handleKeyDown = (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => {
super.handleKeyDown(e);
};
onValueChange = (value: string) => {
let parsedValue;
switch (this.props.inputType) {
case "NUMBER":
try {
if (value === "") {
parsedValue = null;
} else if (value === "-") {
parsedValue = "-";
} else if (/\.$/.test(value)) {
parsedValue = value;
} else {
parsedValue = Number(value);
if (isNaN(parsedValue)) {
parsedValue = null;
}
}
break;
} catch (e) {
parsedValue = value;
}
break;
case "TEXT":
case "EMAIL":
case "PASSWORD":
parsedValue = value;
break;
}
this.props.updateWidgetMetaProperty("text", parsedValue, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
};
getPageView() {
const value = this.props.text ?? "";
let isInvalid = false;
if (this.props.isDirty) {
isInvalid = "isValid" in this.props && !this.props.isValid;
} else {
isInvalid = false;
}
const conditionalProps: Partial<InputComponentProps> = {};
conditionalProps.errorMessage = this.props.errorMessage;
if (this.props.isRequired && value.length === 0) {
conditionalProps.errorMessage = createMessage(FIELD_REQUIRED_ERROR);
}
if (!isNil(this.props.maxNum)) {
conditionalProps.maxNum = this.props.maxNum;
}
if (!isNil(this.props.minNum)) {
conditionalProps.minNum = this.props.minNum;
}
if (this.props.inputType === "TEXT" && this.props.maxChars) {
// pass maxChars only for Text type inputs, undefined for other types
conditionalProps.maxChars = this.props.maxChars;
if (
this.props.defaultText &&
this.props.defaultText.toString().length > this.props.maxChars
) {
isInvalid = true;
conditionalProps.errorMessage = createMessage(
INPUT_DEFAULT_TEXT_MAX_CHAR_ERROR,
);
}
}
const minInputSingleLineHeight =
this.props.label || this.props.tooltip
? // adjust height for label | tooltip extra div
GRID_DENSITY_MIGRATION_V1 + 4
: // GRID_DENSITY_MIGRATION_V1 used to adjust code as per new scaled canvas.
GRID_DENSITY_MIGRATION_V1;
return (
<InputComponent
autoFocus={this.props.autoFocus}
// show label and Input side by side if true
compactMode={
!(
(this.props.bottomRow - this.props.topRow) /
GRID_DENSITY_MIGRATION_V1 >
1 && this.props.inputType === "TEXT"
)
}
defaultValue={this.props.defaultText}
disableNewLineOnPressEnterKey={!!this.props.onSubmit}
disabled={this.props.isDisabled}
iconAlign={this.props.iconAlign}
iconName={this.props.iconName}
inputType={this.props.inputType}
isInvalid={isInvalid}
isLoading={this.props.isLoading}
label={this.props.label}
labelStyle={this.props.labelStyle}
labelTextColor={this.props.labelTextColor}
labelTextSize={this.props.labelTextSize}
multiline={
(this.props.bottomRow - this.props.topRow) /
minInputSingleLineHeight >
1 && this.props.inputType === "TEXT"
}
onFocusChange={this.handleFocusChange}
onKeyDown={this.handleKeyDown}
onValueChange={this.onValueChange}
placeholder={this.props.placeholderText}
showError={!!this.props.isFocused}
spellCheck={!!this.props.isSpellCheck}
stepSize={1}
tooltip={this.props.tooltip}
value={value}
widgetId={this.props.widgetId}
{...conditionalProps}
/>
);
}
static getWidgetType(): WidgetType {
return "INPUT_WIDGET_V2";
}
}
export interface InputWidgetProps extends BaseInputWidgetProps {
defaultText?: string | number;
maxChars?: number;
isSpellCheck?: boolean;
maxNum?: number;
minNum?: number;
}
export default InputWidget;