mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
make asyncValidators run only before onSubmit
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
efedf2acd2
commit
c7148bb980
@ -93,7 +93,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
validators = {isPath}
|
asyncValidators={isPath}
|
||||||
className="box grow"
|
className="box grow"
|
||||||
value={this.getFilePath(fileType)}
|
value={this.getFilePath(fileType)}
|
||||||
onChange={v => this.setFilepath(fileType, v)}
|
onChange={v => this.setFilepath(fileType, v)}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const KubectlBinaries = observer(() => {
|
|||||||
theme="round-black"
|
theme="round-black"
|
||||||
value={userStore.downloadBinariesPath}
|
value={userStore.downloadBinariesPath}
|
||||||
placeholder={getDefaultKubectlPath()}
|
placeholder={getDefaultKubectlPath()}
|
||||||
validators={pathValidator}
|
asyncValidators={pathValidator}
|
||||||
onChange={setDownloadPath}
|
onChange={setDownloadPath}
|
||||||
onBlur={save}
|
onBlur={save}
|
||||||
disabled={!userStore.downloadKubectlBinaries}
|
disabled={!userStore.downloadKubectlBinaries}
|
||||||
@ -79,7 +79,7 @@ export const KubectlBinaries = observer(() => {
|
|||||||
theme="round-black"
|
theme="round-black"
|
||||||
placeholder={bundledKubectlPath()}
|
placeholder={bundledKubectlPath()}
|
||||||
value={binariesPath}
|
value={binariesPath}
|
||||||
validators={pathValidator}
|
asyncValidators={pathValidator}
|
||||||
onChange={setBinariesPath}
|
onChange={setBinariesPath}
|
||||||
onBlur={save}
|
onBlur={save}
|
||||||
disabled={userStore.downloadKubectlBinaries}
|
disabled={userStore.downloadKubectlBinaries}
|
||||||
|
|||||||
@ -20,7 +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}
|
validators={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);
|
||||||
@ -32,7 +32,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.
|
This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.
|
||||||
</small>
|
</small>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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, InputValidator } from "../input";
|
import { Input, InputValidator, AsyncInputValidator } 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,7 +12,8 @@ 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,
|
validators?: InputValidator | InputValidator[],
|
||||||
|
asyncValidators?: AsyncInputValidator | AsyncInputValidator[];
|
||||||
|
|
||||||
// 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`
|
||||||
@ -40,7 +41,7 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { items, remove, renderItem, placeholder, validator } = this.props;
|
const { items, remove, renderItem, placeholder, validators, asyncValidators } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="EditableList">
|
<div className="EditableList">
|
||||||
@ -48,7 +49,8 @@ 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}
|
validators={validators}
|
||||||
|
asyncValidators={asyncValidators}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={val => this.currentNewItem = val}
|
onChange={val => this.currentNewItem = val}
|
||||||
|
|||||||
@ -23,6 +23,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.waiting {
|
||||||
|
pointer-events: none;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: transparentize(white, .85);
|
||||||
|
animation: waiting 1.5s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
--flex-gap: #{$padding / 1.5};
|
--flex-gap: #{$padding / 1.5};
|
||||||
|
|
||||||
@ -123,3 +137,18 @@
|
|||||||
--border: none;
|
--border: none;
|
||||||
--color: white;
|
--color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes waiting {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
left: 25%;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
left: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ 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 { AsyncInputValidator, InputValidator, ValidatorMessage } from "./input_validators";
|
||||||
import isString from "lodash/isString";
|
import isString from "lodash/isString";
|
||||||
import { action, computed, observable } from "mobx";
|
import { action, computed, observable } from "mobx";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
@ -13,7 +13,7 @@ import { observer } from "mobx-react";
|
|||||||
|
|
||||||
const { conditionalValidators, ...InputValidators } = Validators;
|
const { conditionalValidators, ...InputValidators } = Validators;
|
||||||
|
|
||||||
export { InputValidators, InputValidator };
|
export { InputValidators, InputValidator, AsyncInputValidator };
|
||||||
|
|
||||||
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||||
type InputElementProps = InputHTMLAttributes<InputElement> & TextareaHTMLAttributes<InputElement> & DOMAttributes<InputElement>;
|
type InputElementProps = InputHTMLAttributes<InputElement> & TextareaHTMLAttributes<InputElement> & DOMAttributes<InputElement>;
|
||||||
@ -31,6 +31,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
|||||||
iconRight?: string | React.ReactNode;
|
iconRight?: string | React.ReactNode;
|
||||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||||
validators?: InputValidator | InputValidator[];
|
validators?: InputValidator | InputValidator[];
|
||||||
|
asyncValidators?: AsyncInputValidator | AsyncInputValidator[];
|
||||||
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
||||||
onSubmit?(value: T): void;
|
onSubmit?(value: T): void;
|
||||||
};
|
};
|
||||||
@ -48,24 +49,31 @@ export class Input extends React.Component<InputProps> {
|
|||||||
|
|
||||||
public inputRef = React.createRef<InputElement>();
|
public inputRef = React.createRef<InputElement>();
|
||||||
public validators: InputValidator[] = [];
|
public validators: InputValidator[] = [];
|
||||||
|
public asyncValidators: AsyncInputValidator[] = [];
|
||||||
|
|
||||||
@observable errors: React.ReactNode[] = [];
|
@observable errors: React.ReactNode[] = [];
|
||||||
@observable dirty = Boolean(this.props.showErrorInitially);
|
@observable dirty = Boolean(this.props.showErrorInitially);
|
||||||
@observable focused = false;
|
@observable focused = false;
|
||||||
|
@observable asyncValidating = false;
|
||||||
|
@observable isSubmitting = false;
|
||||||
|
|
||||||
@computed get isValid() {
|
@computed get isValid() {
|
||||||
return this.errors.length === 0;
|
return this.errors.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { validators, asyncValidators, showErrorInitially } = this.props;
|
||||||
|
|
||||||
this.validators = conditionalValidators
|
this.validators = conditionalValidators
|
||||||
// add conditional validators if matches input props
|
// add conditional validators if matches input props
|
||||||
.filter(validator => validator.condition(this.props))
|
.filter(validator => validator.condition(this.props))
|
||||||
// add custom validators
|
// add custom validators
|
||||||
.concat(this.props.validators);
|
.concat(validators);
|
||||||
|
|
||||||
if (this.props.showErrorInitially) {
|
this.asyncValidators = [asyncValidators].flat();
|
||||||
this.runValidatorsRaw();
|
|
||||||
|
if (showErrorInitially) {
|
||||||
|
this.runValidatorsRaw(this.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoFitHeight();
|
this.autoFitHeight();
|
||||||
@ -118,10 +126,44 @@ export class Input extends React.Component<InputProps> {
|
|||||||
textArea.style.height = `${height}px`;
|
textArea.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveValidatorMessage(message: ValidatorMessage, value: string): React.ReactNode {
|
||||||
|
return typeof message === "function"
|
||||||
|
? message(value, this.props)
|
||||||
|
: message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function should only be run before submitting.
|
||||||
|
*/
|
||||||
|
async runAsyncValidators(value: string): Promise<React.ReactNode[]> {
|
||||||
|
if (this.asyncValidators.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.asyncValidating = true;
|
||||||
|
|
||||||
|
return (await Promise.all(
|
||||||
|
this.asyncValidators.map(validator => (
|
||||||
|
validator.validate(value, this.props)
|
||||||
|
.then(isValid => {
|
||||||
|
if (!isValid) {
|
||||||
|
return [this.resolveValidatorMessage(validator.message, value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.catch(error => Promise.resolve<React.ReactNode[]>([error, this.resolveValidatorMessage(validator.message, value)]))
|
||||||
|
)),
|
||||||
|
)).flat();
|
||||||
|
} finally {
|
||||||
|
this.asyncValidating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
runValidatorsRaw() {
|
runValidatorsRaw(value: string) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
const value = this.getValue();
|
|
||||||
|
|
||||||
// run validators
|
// run validators
|
||||||
for (const validator of this.validators) {
|
for (const validator of this.validators) {
|
||||||
@ -139,7 +181,7 @@ export class Input extends React.Component<InputProps> {
|
|||||||
this.inputRef.current.setCustomValidity(this.errors.length ? this.errors[0].toString() : "");
|
this.inputRef.current.setCustomValidity(this.errors.length ? this.errors[0].toString() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
runValidators = debounce(() => this.runValidatorsRaw(), 500, {
|
runValidators = debounce(() => this.runValidatorsRaw(this.getValue()), 500, {
|
||||||
trailing: true,
|
trailing: true,
|
||||||
leading: false,
|
leading: false,
|
||||||
});
|
});
|
||||||
@ -188,11 +230,25 @@ export class Input extends React.Component<InputProps> {
|
|||||||
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
||||||
|
|
||||||
if (!modified && evt.key === "Enter") {
|
if (!modified && evt.key === "Enter") {
|
||||||
this.runValidatorsRaw();
|
const value = this.getValue();
|
||||||
|
|
||||||
if (this.isValid) {
|
this.isSubmitting = true;
|
||||||
this.props.onSubmit?.(this.getValue());
|
this.runValidatorsRaw(value);
|
||||||
|
|
||||||
|
if (!this.isValid) {
|
||||||
|
return this.isSubmitting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.runAsyncValidators(value)
|
||||||
|
.then(errors => {
|
||||||
|
this.errors.push(...errors);
|
||||||
|
|
||||||
|
if (this.isValid) {
|
||||||
|
this.props.onSubmit?.(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +266,7 @@ export class Input extends React.Component<InputProps> {
|
|||||||
const {
|
const {
|
||||||
multiLine, 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,
|
||||||
onChange, onSubmit, showErrorInitially, ...inputPropsRaw
|
onChange, onSubmit, asyncValidators, showErrorInitially, ...inputPropsRaw
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const className = cssNames("Input", this.props.className, {
|
const className = cssNames("Input", this.props.className, {
|
||||||
[`theme ${theme}`]: theme,
|
[`theme ${theme}`]: theme,
|
||||||
@ -218,6 +274,7 @@ export class Input extends React.Component<InputProps> {
|
|||||||
disabled,
|
disabled,
|
||||||
invalid: !this.isValid,
|
invalid: !this.isValid,
|
||||||
dirty: this.dirty,
|
dirty: this.dirty,
|
||||||
|
waiting: this.asyncValidating,
|
||||||
});
|
});
|
||||||
const inputProps: InputElementProps = {
|
const inputProps: InputElementProps = {
|
||||||
...inputPropsRaw,
|
...inputPropsRaw,
|
||||||
@ -227,7 +284,7 @@ export class Input extends React.Component<InputProps> {
|
|||||||
onChange: this.onChange,
|
onChange: this.onChange,
|
||||||
onKeyDown: this.onKeyDown,
|
onKeyDown: this.onKeyDown,
|
||||||
spellCheck: "false",
|
spellCheck: "false",
|
||||||
disabled,
|
disabled: disabled || this.isSubmitting,
|
||||||
};
|
};
|
||||||
const showErrors = this.errors.length > 0;
|
const showErrors = this.errors.length > 0;
|
||||||
const errorsInfo = (
|
const errorsInfo = (
|
||||||
|
|||||||
@ -2,12 +2,20 @@ import type { InputProps } from "./input";
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
|
||||||
|
export type ValidatorMessage = ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
||||||
|
|
||||||
export interface InputValidator {
|
export interface InputValidator {
|
||||||
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: ValidatorMessage;
|
||||||
validate(value: string, props?: InputProps): boolean;
|
validate(value: string, props?: InputProps): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AsyncInputValidator {
|
||||||
|
condition?(props: InputProps): boolean; // auto-bind condition depending on input props
|
||||||
|
message: ValidatorMessage;
|
||||||
|
validate(value: string, props?: InputProps): Promise<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",
|
||||||
@ -54,10 +62,18 @@ export const isExtensionNameInstall: InputValidator = {
|
|||||||
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPath: InputValidator = {
|
export const isPath: AsyncInputValidator = {
|
||||||
condition: ({ type }) => type === "text",
|
condition: ({ type }) => type === "text",
|
||||||
message: "This field must be a path to an existing file.",
|
message: "This field must be a path to an existing file.",
|
||||||
validate: value => value && fse.pathExistsSync(value),
|
validate: async value => {
|
||||||
|
try {
|
||||||
|
await fse.stat(value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const minLength: InputValidator = {
|
export const minLength: InputValidator = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user