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">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
validators = {isPath}
|
||||
asyncValidators={isPath}
|
||||
className="box grow"
|
||||
value={this.getFilePath(fileType)}
|
||||
onChange={v => this.setFilepath(fileType, v)}
|
||||
|
||||
@ -61,7 +61,7 @@ export const KubectlBinaries = observer(() => {
|
||||
theme="round-black"
|
||||
value={userStore.downloadBinariesPath}
|
||||
placeholder={getDefaultKubectlPath()}
|
||||
validators={pathValidator}
|
||||
asyncValidators={pathValidator}
|
||||
onChange={setDownloadPath}
|
||||
onBlur={save}
|
||||
disabled={!userStore.downloadKubectlBinaries}
|
||||
@ -79,7 +79,7 @@ export const KubectlBinaries = observer(() => {
|
||||
theme="round-black"
|
||||
placeholder={bundledKubectlPath()}
|
||||
value={binariesPath}
|
||||
validators={pathValidator}
|
||||
asyncValidators={pathValidator}
|
||||
onChange={setBinariesPath}
|
||||
onBlur={save}
|
||||
disabled={userStore.downloadKubectlBinaries}
|
||||
|
||||
@ -20,7 +20,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
|
||||
<EditableList
|
||||
placeholder="Add new namespace..."
|
||||
validator={namespaceValue}
|
||||
validators={namespaceValue}
|
||||
add={(newNamespace) => {
|
||||
this.namespaces.add(newNamespace);
|
||||
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
||||
@ -32,7 +32,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import "./editable-list.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { Input, InputValidator } from "../input";
|
||||
import { Input, InputValidator, AsyncInputValidator } from "../input";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { autobind } from "../../utils";
|
||||
@ -12,7 +12,8 @@ export interface Props<T> {
|
||||
add: (newItem: string) => void,
|
||||
remove: (info: { oldItem: T, index: number }) => void,
|
||||
placeholder?: string,
|
||||
validator?: InputValidator,
|
||||
validators?: InputValidator | InputValidator[],
|
||||
asyncValidators?: AsyncInputValidator | AsyncInputValidator[];
|
||||
|
||||
// An optional prop used to convert T to a displayable string
|
||||
// defaults to `String`
|
||||
@ -40,7 +41,7 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, remove, renderItem, placeholder, validator } = this.props;
|
||||
const { items, remove, renderItem, placeholder, validators, asyncValidators } = this.props;
|
||||
|
||||
return (
|
||||
<div className="EditableList">
|
||||
@ -48,7 +49,8 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.currentNewItem}
|
||||
validators={validator}
|
||||
validators={validators}
|
||||
asyncValidators={asyncValidators}
|
||||
onSubmit={this.onSubmit}
|
||||
placeholder={placeholder}
|
||||
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 {
|
||||
--flex-gap: #{$padding / 1.5};
|
||||
|
||||
@ -123,3 +137,18 @@
|
||||
--border: none;
|
||||
--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 { Tooltip, TooltipProps } from "../tooltip";
|
||||
import * as Validators from "./input_validators";
|
||||
import { InputValidator } from "./input_validators";
|
||||
import { AsyncInputValidator, InputValidator, ValidatorMessage } from "./input_validators";
|
||||
import isString from "lodash/isString";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { debounce } from "lodash";
|
||||
@ -13,7 +13,7 @@ import { observer } from "mobx-react";
|
||||
|
||||
const { conditionalValidators, ...InputValidators } = Validators;
|
||||
|
||||
export { InputValidators, InputValidator };
|
||||
export { InputValidators, InputValidator, AsyncInputValidator };
|
||||
|
||||
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
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;
|
||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||
validators?: InputValidator | InputValidator[];
|
||||
asyncValidators?: AsyncInputValidator | AsyncInputValidator[];
|
||||
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
||||
onSubmit?(value: T): void;
|
||||
};
|
||||
@ -48,24 +49,31 @@ export class Input extends React.Component<InputProps> {
|
||||
|
||||
public inputRef = React.createRef<InputElement>();
|
||||
public validators: InputValidator[] = [];
|
||||
public asyncValidators: AsyncInputValidator[] = [];
|
||||
|
||||
@observable errors: React.ReactNode[] = [];
|
||||
@observable dirty = Boolean(this.props.showErrorInitially);
|
||||
@observable focused = false;
|
||||
@observable asyncValidating = false;
|
||||
@observable isSubmitting = false;
|
||||
|
||||
@computed get isValid() {
|
||||
return this.errors.length === 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { validators, asyncValidators, showErrorInitially } = this.props;
|
||||
|
||||
this.validators = conditionalValidators
|
||||
// add conditional validators if matches input props
|
||||
.filter(validator => validator.condition(this.props))
|
||||
// add custom validators
|
||||
.concat(this.props.validators);
|
||||
.concat(validators);
|
||||
|
||||
if (this.props.showErrorInitially) {
|
||||
this.runValidatorsRaw();
|
||||
this.asyncValidators = [asyncValidators].flat();
|
||||
|
||||
if (showErrorInitially) {
|
||||
this.runValidatorsRaw(this.getValue());
|
||||
}
|
||||
|
||||
this.autoFitHeight();
|
||||
@ -118,10 +126,44 @@ export class Input extends React.Component<InputProps> {
|
||||
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
|
||||
runValidatorsRaw() {
|
||||
runValidatorsRaw(value: string) {
|
||||
this.errors = [];
|
||||
const value = this.getValue();
|
||||
|
||||
// run 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() : "");
|
||||
}
|
||||
|
||||
runValidators = debounce(() => this.runValidatorsRaw(), 500, {
|
||||
runValidators = debounce(() => this.runValidatorsRaw(this.getValue()), 500, {
|
||||
trailing: true,
|
||||
leading: false,
|
||||
});
|
||||
@ -188,11 +230,25 @@ export class Input extends React.Component<InputProps> {
|
||||
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
||||
|
||||
if (!modified && evt.key === "Enter") {
|
||||
this.runValidatorsRaw();
|
||||
const value = this.getValue();
|
||||
|
||||
if (this.isValid) {
|
||||
this.props.onSubmit?.(this.getValue());
|
||||
this.isSubmitting = true;
|
||||
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 {
|
||||
multiLine, validators, theme, maxRows, children, showErrorsAsTooltip,
|
||||
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
|
||||
onChange, onSubmit, showErrorInitially, ...inputPropsRaw
|
||||
onChange, onSubmit, asyncValidators, showErrorInitially, ...inputPropsRaw
|
||||
} = this.props;
|
||||
const className = cssNames("Input", this.props.className, {
|
||||
[`theme ${theme}`]: theme,
|
||||
@ -218,6 +274,7 @@ export class Input extends React.Component<InputProps> {
|
||||
disabled,
|
||||
invalid: !this.isValid,
|
||||
dirty: this.dirty,
|
||||
waiting: this.asyncValidating,
|
||||
});
|
||||
const inputProps: InputElementProps = {
|
||||
...inputPropsRaw,
|
||||
@ -227,7 +284,7 @@ export class Input extends React.Component<InputProps> {
|
||||
onChange: this.onChange,
|
||||
onKeyDown: this.onKeyDown,
|
||||
spellCheck: "false",
|
||||
disabled,
|
||||
disabled: disabled || this.isSubmitting,
|
||||
};
|
||||
const showErrors = this.errors.length > 0;
|
||||
const errorsInfo = (
|
||||
|
||||
@ -2,12 +2,20 @@ import type { InputProps } from "./input";
|
||||
import { ReactNode } from "react";
|
||||
import fse from "fs-extra";
|
||||
|
||||
export type ValidatorMessage = ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
||||
|
||||
export interface InputValidator {
|
||||
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;
|
||||
}
|
||||
|
||||
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 = {
|
||||
condition: ({ required }) => required,
|
||||
message: "This field is required",
|
||||
@ -54,10 +62,18 @@ export const isExtensionNameInstall: InputValidator = {
|
||||
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
||||
};
|
||||
|
||||
export const isPath: InputValidator = {
|
||||
export const isPath: AsyncInputValidator = {
|
||||
condition: ({ type }) => type === "text",
|
||||
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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user