mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor <Input> to be clearer. Add namespace name validator
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
9e75016247
commit
efedf2acd2
@ -4,6 +4,7 @@ import { Cluster } from "../../../../main/cluster";
|
|||||||
import { SubTitle } from "../../layout/sub-title";
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
import { EditableList } from "../../editable-list";
|
import { EditableList } from "../../editable-list";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
|
import { namespaceValue } from "../../input/input_validators";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -19,6 +20,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
|||||||
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
|
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
|
||||||
<EditableList
|
<EditableList
|
||||||
placeholder="Add new namespace..."
|
placeholder="Add new namespace..."
|
||||||
|
validator={namespaceValue}
|
||||||
add={(newNamespace) => {
|
add={(newNamespace) => {
|
||||||
this.namespaces.add(newNamespace);
|
this.namespaces.add(newNamespace);
|
||||||
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import "./editable-list.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Input } from "../input";
|
import { Input, InputValidator } from "../input";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
@ -12,6 +12,7 @@ export interface Props<T> {
|
|||||||
add: (newItem: string) => void,
|
add: (newItem: string) => void,
|
||||||
remove: (info: { oldItem: T, index: number }) => void,
|
remove: (info: { oldItem: T, index: number }) => void,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
|
validator?: InputValidator,
|
||||||
|
|
||||||
// An optional prop used to convert T to a displayable string
|
// An optional prop used to convert T to a displayable string
|
||||||
// defaults to `String`
|
// defaults to `String`
|
||||||
@ -39,7 +40,7 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { items, remove, renderItem, placeholder } = this.props;
|
const { items, remove, renderItem, placeholder, validator } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="EditableList">
|
<div className="EditableList">
|
||||||
@ -47,6 +48,7 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
|||||||
<Input
|
<Input
|
||||||
theme="round-black"
|
theme="round-black"
|
||||||
value={this.currentNewItem}
|
value={this.currentNewItem}
|
||||||
|
validators={validator}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={val => this.currentNewItem = val}
|
onChange={val => this.currentNewItem = val}
|
||||||
|
|||||||
@ -39,8 +39,8 @@ export class HotbarAddCommand extends React.Component {
|
|||||||
data-test-id="command-palette-hotbar-add-name"
|
data-test-id="command-palette-hotbar-add-name"
|
||||||
validators={[uniqueHotbarName]}
|
validators={[uniqueHotbarName]}
|
||||||
onSubmit={(v) => this.onSubmit(v)}
|
onSubmit={(v) => this.onSubmit(v)}
|
||||||
dirty={true}
|
showErrorInitially={true}
|
||||||
showValidationLine={true} />
|
/>
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel)
|
Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel)
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import "./input.scss";
|
import "./input.scss";
|
||||||
|
|
||||||
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||||
import { autobind, cssNames, debouncePromise, getRandId } from "../../utils";
|
import { autobind, cssNames, getRandId } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Tooltip, TooltipProps } from "../tooltip";
|
import { Tooltip, TooltipProps } from "../tooltip";
|
||||||
import * as Validators from "./input_validators";
|
import * as Validators from "./input_validators";
|
||||||
import { InputValidator } from "./input_validators";
|
import { InputValidator } from "./input_validators";
|
||||||
import isString from "lodash/isString";
|
import isString from "lodash/isString";
|
||||||
import isFunction from "lodash/isFunction";
|
import { action, computed, observable } from "mobx";
|
||||||
import isBoolean from "lodash/isBoolean";
|
import { debounce } from "lodash";
|
||||||
import uniqueId from "lodash/uniqueId";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
const { conditionalValidators, ...InputValidators } = Validators;
|
const { conditionalValidators, ...InputValidators } = Validators;
|
||||||
|
|
||||||
@ -25,8 +25,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
|||||||
autoSelectOnFocus?: boolean
|
autoSelectOnFocus?: boolean
|
||||||
multiLine?: boolean; // use text-area as input field
|
multiLine?: boolean; // use text-area as input field
|
||||||
maxRows?: number; // when multiLine={true} define max rows size
|
maxRows?: number; // when multiLine={true} define max rows size
|
||||||
dirty?: boolean; // show validation errors even if the field wasn't touched yet
|
showErrorInitially?: boolean; // show validation errors even if the field wasn't touched yet
|
||||||
showValidationLine?: boolean; // show animated validation line for async validators
|
|
||||||
showErrorsAsTooltip?: boolean | Omit<TooltipProps, "targetId">; // show validation errors as a tooltip :hover (instead of block below)
|
showErrorsAsTooltip?: boolean | Omit<TooltipProps, "targetId">; // show validation errors as a tooltip :hover (instead of block below)
|
||||||
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
|
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
|
||||||
iconRight?: string | React.ReactNode;
|
iconRight?: string | React.ReactNode;
|
||||||
@ -36,46 +35,50 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
|||||||
onSubmit?(value: T): void;
|
onSubmit?(value: T): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface State {
|
|
||||||
focused?: boolean;
|
|
||||||
dirty?: boolean;
|
|
||||||
dirtyOnBlur?: boolean;
|
|
||||||
valid?: boolean;
|
|
||||||
validating?: boolean;
|
|
||||||
errors?: React.ReactNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps: Partial<InputProps> = {
|
const defaultProps: Partial<InputProps> = {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
maxRows: 10000,
|
maxRows: 10000,
|
||||||
showValidationLine: true,
|
|
||||||
validators: [],
|
validators: [],
|
||||||
|
showErrorInitially: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Input extends React.Component<InputProps, State> {
|
@observer
|
||||||
|
export class Input extends React.Component<InputProps> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
public input: InputElement;
|
public inputRef = React.createRef<InputElement>();
|
||||||
public validators: InputValidator[] = [];
|
public validators: InputValidator[] = [];
|
||||||
|
|
||||||
public state: State = {
|
@observable errors: React.ReactNode[] = [];
|
||||||
dirty: !!this.props.dirty,
|
@observable dirty = Boolean(this.props.showErrorInitially);
|
||||||
valid: true,
|
@observable focused = false;
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
isValid() {
|
@computed get isValid() {
|
||||||
return this.state.valid;
|
return this.errors.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.validators = conditionalValidators
|
||||||
|
// add conditional validators if matches input props
|
||||||
|
.filter(validator => validator.condition(this.props))
|
||||||
|
// add custom validators
|
||||||
|
.concat(this.props.validators);
|
||||||
|
|
||||||
|
if (this.props.showErrorInitially) {
|
||||||
|
this.runValidatorsRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoFitHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value: string) {
|
setValue(value: string) {
|
||||||
if (value !== this.getValue()) {
|
if (value !== this.getValue()) {
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.inputRef.constructor.prototype, "value").set;
|
||||||
|
|
||||||
nativeInputValueSetter.call(this.input, value);
|
nativeInputValueSetter.call(this.inputRef, value);
|
||||||
const evt = new Event("input", { bubbles: true });
|
const evt = new Event("input", { bubbles: true });
|
||||||
|
|
||||||
this.input.dispatchEvent(evt);
|
this.inputRef.current.dispatchEvent(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,21 +86,21 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
const { value, defaultValue = "" } = this.props;
|
const { value, defaultValue = "" } = this.props;
|
||||||
|
|
||||||
if (value !== undefined) return value; // controlled input
|
if (value !== undefined) return value; // controlled input
|
||||||
if (this.input) return this.input.value; // uncontrolled input
|
if (this.inputRef) return this.inputRef.current.value; // uncontrolled input
|
||||||
|
|
||||||
return defaultValue as string;
|
return defaultValue as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.input.focus();
|
this.inputRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
blur() {
|
blur() {
|
||||||
this.input.blur();
|
this.inputRef.current.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
select() {
|
select() {
|
||||||
this.input.select();
|
this.inputRef.current.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
private autoFitHeight() {
|
private autoFitHeight() {
|
||||||
@ -106,7 +109,8 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
if (!multiLine) {
|
if (!multiLine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const textArea = this.input;
|
|
||||||
|
const textArea = this.inputRef.current;
|
||||||
const lineHeight = parseFloat(window.getComputedStyle(textArea).lineHeight);
|
const lineHeight = parseFloat(window.getComputedStyle(textArea).lineHeight);
|
||||||
const rowsCount = (this.getValue().match(/\n/g) || []).length + 1;
|
const rowsCount = (this.getValue().match(/\n/g) || []).length + 1;
|
||||||
const height = lineHeight * Math.min(Math.max(rowsCount, rows), maxRows);
|
const height = lineHeight * Math.min(Math.max(rowsCount, rows), maxRows);
|
||||||
@ -114,117 +118,62 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
textArea.style.height = `${height}px`;
|
textArea.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private validationId: string;
|
@action
|
||||||
|
runValidatorsRaw() {
|
||||||
async validate(value = this.getValue()) {
|
this.errors = [];
|
||||||
let validationId = (this.validationId = ""); // reset every time for async validators
|
const value = this.getValue();
|
||||||
const asyncValidators: Promise<any>[] = [];
|
|
||||||
const errors: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
// run validators
|
// run validators
|
||||||
for (const validator of this.validators) {
|
for (const validator of this.validators) {
|
||||||
if (errors.length) {
|
const isValid = validator.validate(value, this.props);
|
||||||
// stop validation check if there is an error already
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const result = validator.validate(value, this.props);
|
|
||||||
|
|
||||||
if (isBoolean(result) && !result) {
|
if (!isValid) {
|
||||||
errors.push(this.getValidatorError(value, validator));
|
if (typeof validator.message === "function") {
|
||||||
} else if (result instanceof Promise) {
|
this.errors.push(validator.message(value, this.props));
|
||||||
if (!validationId) {
|
} else {
|
||||||
this.validationId = validationId = uniqueId("validation_id_");
|
this.errors.push(validator.message);
|
||||||
}
|
}
|
||||||
asyncValidators.push(
|
|
||||||
result.then(
|
|
||||||
() => null, // don't consider any valid result from promise since we interested in errors only
|
|
||||||
error => this.getValidatorError(value, validator) || error
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save sync validators result first
|
this.inputRef.current.setCustomValidity(this.errors.length ? this.errors[0].toString() : "");
|
||||||
this.setValidation(errors);
|
|
||||||
|
|
||||||
// handle async validators result
|
|
||||||
if (asyncValidators.length > 0) {
|
|
||||||
this.setState({ validating: true, valid: false });
|
|
||||||
const asyncErrors = await Promise.all(asyncValidators);
|
|
||||||
|
|
||||||
if (this.validationId === validationId) {
|
|
||||||
this.setValidation(errors.concat(...asyncErrors.filter(err => err)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.input.setCustomValidity(errors.length ? errors[0].toString() : "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setValidation(errors: React.ReactNode[]) {
|
runValidators = debounce(() => this.runValidatorsRaw(), 500, {
|
||||||
this.setState({
|
trailing: true,
|
||||||
validating: false,
|
leading: false,
|
||||||
valid: !errors.length,
|
});
|
||||||
errors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getValidatorError(value: string, { message }: InputValidator) {
|
validate() {
|
||||||
if (isFunction(message)) return message(value, this.props);
|
this.errors = [];
|
||||||
|
this.runValidators();
|
||||||
return message || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupValidators() {
|
|
||||||
this.validators = conditionalValidators
|
|
||||||
// add conditional validators if matches input props
|
|
||||||
.filter(validator => validator.condition(this.props))
|
|
||||||
// add custom validators
|
|
||||||
.concat(this.props.validators)
|
|
||||||
// debounce async validators
|
|
||||||
.map(({ debounce, ...validator }) => {
|
|
||||||
if (debounce) validator.validate = debouncePromise(validator.validate, debounce);
|
|
||||||
|
|
||||||
return validator;
|
|
||||||
});
|
|
||||||
// run validation
|
|
||||||
this.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
setDirty(dirty = true) {
|
|
||||||
if (this.state.dirty === dirty) return;
|
|
||||||
this.setState({ dirty });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onFocus(evt: React.FocusEvent<InputElement>) {
|
onFocus(evt: React.FocusEvent<InputElement>) {
|
||||||
const { onFocus, autoSelectOnFocus } = this.props;
|
const { onFocus, autoSelectOnFocus } = this.props;
|
||||||
|
|
||||||
if (onFocus) onFocus(evt);
|
onFocus?.(evt);
|
||||||
if (autoSelectOnFocus) this.select();
|
|
||||||
this.setState({ focused: true });
|
if (autoSelectOnFocus) {
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onBlur(evt: React.FocusEvent<InputElement>) {
|
onBlur(evt: React.FocusEvent<InputElement>) {
|
||||||
const { onBlur } = this.props;
|
this.props.onBlur?.(evt);
|
||||||
|
this.focused = false;
|
||||||
if (onBlur) onBlur(evt);
|
|
||||||
if (this.state.dirtyOnBlur) this.setState({ dirty: true, dirtyOnBlur: false });
|
|
||||||
this.setState({ focused: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onChange(evt: React.ChangeEvent<any>) {
|
onChange(evt: React.ChangeEvent<InputElement>) {
|
||||||
if (this.props.onChange) {
|
this.props.onChange?.(evt.currentTarget.value, evt);
|
||||||
this.props.onChange(evt.currentTarget.value, evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validate();
|
this.validate();
|
||||||
this.autoFitHeight();
|
this.autoFitHeight();
|
||||||
|
|
||||||
// mark input as dirty for the first time only onBlur() to avoid immediate error-state show when start typing
|
|
||||||
if (!this.state.dirty) this.setState({ dirtyOnBlur: true });
|
|
||||||
|
|
||||||
// re-render component when used as uncontrolled input
|
// re-render component when used as uncontrolled input
|
||||||
// when used @defaultValue instead of @value changing real input.value doesn't call render()
|
// when used @defaultValue instead of @value changing real input.value doesn't call render()
|
||||||
if (this.isUncontrolled && this.showMaxLenIndicator) {
|
if (this.isUncontrolled && this.showMaxLenIndicator) {
|
||||||
@ -233,19 +182,17 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onKeyDown(evt: React.KeyboardEvent<any>) {
|
onKeyDown(evt: React.KeyboardEvent<InputElement>) {
|
||||||
|
this.props.onKeyDown?.(evt);
|
||||||
|
|
||||||
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
||||||
|
|
||||||
if (this.props.onKeyDown) {
|
if (!modified && evt.key === "Enter") {
|
||||||
this.props.onKeyDown(evt);
|
this.runValidatorsRaw();
|
||||||
}
|
|
||||||
|
|
||||||
switch (evt.key) {
|
if (this.isValid) {
|
||||||
case "Enter":
|
this.props.onSubmit?.(this.getValue());
|
||||||
if (this.props.onSubmit && !modified && !evt.repeat && this.isValid) {
|
}
|
||||||
this.props.onSubmit(this.getValue());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,68 +206,33 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
return this.props.value === undefined;
|
return this.props.value === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setupValidators();
|
|
||||||
this.autoFitHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: InputProps) {
|
|
||||||
const { defaultValue, value, dirty, validators } = this.props;
|
|
||||||
|
|
||||||
if (prevProps.value !== value || defaultValue !== prevProps.defaultValue) {
|
|
||||||
this.validate();
|
|
||||||
this.autoFitHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.dirty !== dirty) {
|
|
||||||
this.setDirty(dirty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.validators !== validators) {
|
|
||||||
this.setupValidators();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
bindRef(elem: InputElement) {
|
|
||||||
this.input = elem;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip,
|
multiLine, validators, theme, maxRows, children, showErrorsAsTooltip,
|
||||||
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
|
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
|
||||||
dirty: _dirty, // excluded from passing to input-element
|
onChange, onSubmit, showErrorInitially, ...inputPropsRaw
|
||||||
...inputProps
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { focused, dirty, valid, validating, errors } = this.state;
|
|
||||||
|
|
||||||
const className = cssNames("Input", this.props.className, {
|
const className = cssNames("Input", this.props.className, {
|
||||||
[`theme ${theme}`]: theme,
|
[`theme ${theme}`]: theme,
|
||||||
focused,
|
focused: this.focused,
|
||||||
disabled,
|
disabled,
|
||||||
invalid: !valid,
|
invalid: !this.isValid,
|
||||||
dirty,
|
dirty: this.dirty,
|
||||||
validating,
|
|
||||||
validatingLine: validating && showValidationLine,
|
|
||||||
});
|
});
|
||||||
|
const inputProps: InputElementProps = {
|
||||||
// prepare input props
|
...inputPropsRaw,
|
||||||
Object.assign(inputProps, {
|
|
||||||
className: "input box grow",
|
className: "input box grow",
|
||||||
onFocus: this.onFocus,
|
onFocus: this.onFocus,
|
||||||
onBlur: this.onBlur,
|
onBlur: this.onBlur,
|
||||||
onChange: this.onChange,
|
onChange: this.onChange,
|
||||||
onKeyDown: this.onKeyDown,
|
onKeyDown: this.onKeyDown,
|
||||||
rows: multiLine ? (rows || 1) : null,
|
|
||||||
ref: this.bindRef,
|
|
||||||
spellCheck: "false",
|
spellCheck: "false",
|
||||||
disabled,
|
disabled,
|
||||||
});
|
};
|
||||||
const showErrors = errors.length > 0 && !valid && dirty;
|
const showErrors = this.errors.length > 0;
|
||||||
const errorsInfo = (
|
const errorsInfo = (
|
||||||
<div className="errors box grow">
|
<div className="errors box grow">
|
||||||
{errors.map((error, i) => <p key={i}>{error}</p>)}
|
{this.errors.map((error, i) => <p key={i}>{error}</p>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
const componentId = id || showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
|
const componentId = id || showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
|
||||||
@ -345,7 +257,18 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
{tooltipError}
|
{tooltipError}
|
||||||
<label className="input-area flex gaps align-center" id="">
|
<label className="input-area flex gaps align-center" id="">
|
||||||
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
|
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
|
||||||
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
|
{
|
||||||
|
multiLine
|
||||||
|
? <textarea
|
||||||
|
ref={this.inputRef as React.RefObject<HTMLTextAreaElement>}
|
||||||
|
rows={rows || 1}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
: <input
|
||||||
|
ref={this.inputRef as React.RefObject<HTMLInputElement>}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
|
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
|
||||||
{contentRight}
|
{contentRight}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -3,27 +3,26 @@ import { ReactNode } from "react";
|
|||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
|
||||||
export interface InputValidator {
|
export interface InputValidator {
|
||||||
debounce?: number; // debounce for async validators in ms
|
|
||||||
condition?(props: InputProps): boolean; // auto-bind condition depending on input props
|
condition?(props: InputProps): boolean; // auto-bind condition depending on input props
|
||||||
message?: ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
message: ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
||||||
validate(value: string, props?: InputProps): boolean | Promise<any>; // promise can throw error message
|
validate(value: string, props?: InputProps): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isRequired: InputValidator = {
|
export const isRequired: InputValidator = {
|
||||||
condition: ({ required }) => required,
|
condition: ({ required }) => required,
|
||||||
message: () => `This field is required`,
|
message: "This field is required",
|
||||||
validate: value => !!value.trim(),
|
validate: value => !!value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEmail: InputValidator = {
|
export const isEmail: InputValidator = {
|
||||||
condition: ({ type }) => type === "email",
|
condition: ({ type }) => type === "email",
|
||||||
message: () => `Wrong email format`,
|
message: "Must be an email",
|
||||||
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNumber: InputValidator = {
|
export const isNumber: InputValidator = {
|
||||||
condition: ({ type }) => type === "number",
|
condition: ({ type }) => type === "number",
|
||||||
message: () => `Invalid number`,
|
message: "Must be a number",
|
||||||
validate: (value, { min, max }) => {
|
validate: (value, { min, max }) => {
|
||||||
const numVal = +value;
|
const numVal = +value;
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ export const isNumber: InputValidator = {
|
|||||||
|
|
||||||
export const isUrl: InputValidator = {
|
export const isUrl: InputValidator = {
|
||||||
condition: ({ type }) => type === "url",
|
condition: ({ type }) => type === "url",
|
||||||
message: () => `Wrong url format`,
|
message: "Must be a valid URL",
|
||||||
validate: value => {
|
validate: value => {
|
||||||
try {
|
try {
|
||||||
return Boolean(new URL(value));
|
return Boolean(new URL(value));
|
||||||
@ -51,13 +50,13 @@ export const isExtensionNameInstallRegex = /^(?<name>(@[-\w]+\/)?[-\w]+)(@(?<ver
|
|||||||
|
|
||||||
export const isExtensionNameInstall: InputValidator = {
|
export const isExtensionNameInstall: InputValidator = {
|
||||||
condition: ({ type }) => type === "text",
|
condition: ({ type }) => type === "text",
|
||||||
message: () => "Not an extension name with optional version",
|
message: "Not an extension name with optional version",
|
||||||
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPath: InputValidator = {
|
export const isPath: InputValidator = {
|
||||||
condition: ({ type }) => type === "text",
|
condition: ({ type }) => type === "text",
|
||||||
message: () => `This field must be a valid path`,
|
message: "This field must be a path to an existing file.",
|
||||||
validate: value => value && fse.pathExistsSync(value),
|
validate: value => value && fse.pathExistsSync(value),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,12 +75,17 @@ export const maxLength: InputValidator = {
|
|||||||
const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
|
const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
|
||||||
|
|
||||||
export const systemName: InputValidator = {
|
export const systemName: InputValidator = {
|
||||||
message: () => `A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.`,
|
message: "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.",
|
||||||
|
validate: value => !!value.match(systemNameMatcher),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const namespaceValue: InputValidator = {
|
||||||
|
message: "A Namespace must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.",
|
||||||
validate: value => !!value.match(systemNameMatcher),
|
validate: value => !!value.match(systemNameMatcher),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accountId: InputValidator = {
|
export const accountId: InputValidator = {
|
||||||
message: () => `Invalid account ID`,
|
message: "Invalid account ID",
|
||||||
validate: value => (isEmail.validate(value) || systemName.validate(value))
|
validate: value => (isEmail.validate(value) || systemName.validate(value))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user