1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix async validation errors not being displayed

- And fix validation errors sometimes being displayed multiple times

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-05-17 09:48:04 -04:00
parent 8ce4bceea7
commit 541a2e78d7
5 changed files with 54 additions and 66 deletions

View File

@ -137,6 +137,14 @@ export function hasDefiniteField<Field extends keyof T, T>(field: Field): (val:
return (val): val is T & { [f in Field]-?: NonNullable<T[Field]> } => val[field] != null;
}
export function isPromiseSettledRejected<T>(result: PromiseSettledResult<T>): result is PromiseRejectedResult {
return result.status === "rejected";
}
export function isPromiseSettledFulfilled<T>(result: PromiseSettledResult<T>): result is PromiseFulfilledResult<T> {
return result.status === "fulfilled";
}
export function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
return isObject(error)
&& hasOptionalTypedProperty(error, "code", isString)

View File

@ -14,7 +14,6 @@ import { readFileNotify } from "../read-file-notify/read-file-notify";
import type { InstallRequest } from "../attempt-install/install-request";
import type { ExtensionInfo } from "../attempt-install-by-info.injectable";
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
import { AsyncInputValidationError } from "../../input/input_validators";
export type InstallFromInput = (input: string) => Promise<void>;
@ -51,16 +50,12 @@ export const installFromInput = ({
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
} catch (error) {
if (error instanceof AsyncInputValidationError) {
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
if (extNameCaptures) {
const { name, version } = extNameCaptures;
if (extNameCaptures) {
const { name, version } = extNameCaptures;
return await attemptInstallByInfo({ name, version });
}
} else {
throw error;
return await attemptInstallByInfo({ name, version });
}
}

View File

@ -9,14 +9,12 @@ import { prevDefault } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
import { asyncInputValidator, Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { TooltipPosition } from "../tooltip";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
import extensionInstallationStateStoreInjectable
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import { inputValidator } from "../input/input_validators";
export interface InstallProps {
installPath: string;
@ -30,17 +28,22 @@ interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
const installInputValidators = [
InputValidators.isUrl,
InputValidators.isPath,
InputValidators.isExtensionNameInstall,
];
const installInputValidator = asyncInputValidator({
validate: async (value) => {
if (
InputValidators.isUrl.validate(value)
|| InputValidators.isExtensionNameInstall.validate(value)
) {
return;
}
const installInputValidator = inputValidator({
message: "Invalid URL, absolute path, or extension name",
validate: (value: string, props) => (
installInputValidators.some(({ validate }) => validate(value, props))
),
try {
return await InputValidators.isPath.validate(value);
} catch {
throw new Error("Invalid URL, absolute path, or extension name");
}
},
debounce: InputValidators.isPath.debounce,
});
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({

View File

@ -7,21 +7,19 @@ import "./input.scss";
import type { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
import React from "react";
import { autoBind, cssNames, debouncePromise, getRandId } from "../../utils";
import { autoBind, cssNames, debouncePromise, getRandId, isPromiseSettledFulfilled } from "../../utils";
import { Icon } from "../icon";
import type { TooltipProps } from "../tooltip";
import { Tooltip } from "../tooltip";
import * as Validators from "./input_validators";
import type { InputValidator, InputValidation, InputValidationResult, SyncValidationMessageBuilder } from "./input_validators";
import isFunction from "lodash/isFunction";
import uniqueId from "lodash/uniqueId";
import { debounce } from "lodash";
const { conditionalValidators, AsyncInputValidationError, asyncInputValidator, inputValidator, ...InputValidators } = Validators;
const { conditionalValidators, asyncInputValidator, inputValidator, ...InputValidators } = Validators;
export {
InputValidators,
AsyncInputValidationError,
asyncInputValidator,
inputValidator,
};
@ -165,7 +163,7 @@ export class Input extends React.Component<InputProps, State> {
async validate() {
const value = this.getValue();
let validationId = (this.validationId = ""); // reset every time for async validators
const asyncValidators: Promise<any>[] = [];
const asyncValidators: Promise<React.ReactNode>[] = [];
const errors: React.ReactNode[] = [];
// run validators
@ -179,28 +177,13 @@ export class Input extends React.Component<InputProps, State> {
if (!validationId) {
this.validationId = validationId = uniqueId("validation_id_");
}
asyncValidators.push(
validator.validate(value, this.props).then(
() => null, // don't consider any valid result from promise since we interested in errors only
error => this.getValidatorError(value, validator) || error,
),
);
}
const isValid = validator.validate(value, this.props);
if (isValid === false) {
errors.push(this.getValidatorError(value, validator));
} else if (isValid instanceof Promise) {
if (!validationId) {
this.validationId = validationId = uniqueId("validation_id_");
}
asyncValidators.push(
isValid.then(
() => null, // don't consider any valid result from promise since we interested in errors only
error => this.getValidatorError(value, validator) || error,
),
);
asyncValidators.push((async () => {
try {
await validator.validate(value, this.props);
} catch (error) {
return this.getValidatorError(value, validator) || (error instanceof Error ? error.message : String(error));
}
})());
}
}
@ -210,10 +193,15 @@ export class Input extends React.Component<InputProps, State> {
// handle async validators result
if (asyncValidators.length > 0) {
this.setState({ validating: true, valid: false });
const asyncErrors = await Promise.all(asyncValidators);
const asyncErrors = await Promise.allSettled(asyncValidators);
if (this.validationId === validationId) {
this.setValidation(errors.concat(...asyncErrors.filter(err => err)));
errors.push(...asyncErrors
.filter(isPromiseSettledFulfilled)
.map(res => res.value)
.filter(Boolean));
this.setValidation(errors);
}
}
@ -229,9 +217,9 @@ export class Input extends React.Component<InputProps, State> {
}
private getValidatorError(value: string, { message }: InputValidator) {
if (isFunction(message)) return message(value, this.props);
return message || "";
return typeof message === "function"
? message(value, this.props)
: message;
}
private setupValidators() {

View File

@ -8,9 +8,6 @@ import type { ReactNode } from "react";
import fse from "fs-extra";
import { TypedRegEx } from "typed-regex";
export class AsyncInputValidationError extends Error {
}
export type InputValidationResult<IsAsync extends boolean> =
IsAsync extends true
? Promise<void>
@ -42,12 +39,11 @@ export type InputValidator<IsAsync extends boolean = boolean, RequireProps exten
}
: {
/**
* If asyncronous then the rejection message is the error message
*
* This function MUST reject with an instance of {@link AsyncInputValidationError}
* The validation message maybe either specified from the `message` field (higher priority)
* or if that is not provided then the message will retrived from the rejected with value
*/
validate: InputValidation<true, RequireProps>;
message?: undefined;
message?: ReactNode | SyncValidationMessageBuilder<RequireProps>;
debounce: number;
}
);
@ -131,10 +127,8 @@ export const isPath = asyncInputValidator({
debounce: 100,
condition: ({ type }) => type === "text",
validate: async value => {
try {
await fse.pathExists(value);
} catch {
throw new AsyncInputValidationError(`${value} is not a valid file path`);
if (!await fse.pathExists(value)) {
throw new Error(`"${value}" is not a valid file path`);
}
},
});