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:
parent
8ce4bceea7
commit
541a2e78d7
@ -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)
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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> = ({
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user