Add alert to show validation errors

This commit is contained in:
Koen Vlaswinkel
2023-10-10 14:29:06 +02:00
parent 6b965afe4f
commit 5f4e9d4879
5 changed files with 167 additions and 3 deletions

View File

@@ -69,3 +69,35 @@ MultipleModelingsModeledMultiple.args = {
showMultipleModels: true,
modelingStatus: "saved",
};
export const MultipleModelingsValidationFailedNeutral = Template.bind({});
MultipleModelingsValidationFailedNeutral.args = {
method,
modeledMethods: [
createModeledMethod(method),
createModeledMethod({
...method,
type: "neutral",
}),
],
showMultipleModels: true,
modelingStatus: "unsaved",
};
export const MultipleModelingsValidationFailedDuplicate = Template.bind({});
MultipleModelingsValidationFailedDuplicate.args = {
method,
modeledMethods: [
createModeledMethod(method),
createModeledMethod({
...method,
type: "source",
input: "",
output: "ReturnValue",
kind: "remote",
}),
createModeledMethod(method),
],
showMultipleModels: true,
modelingStatus: "unsaved",
};

View File

@@ -85,11 +85,28 @@ type Props = {
// Inverse the color scheme
inverse?: boolean;
/**
* Role is used as the ARIA role. "alert" should only be set if the alert requires
* the user's immediate attention. "status" should be set if the alert is not
* important enough to require the user's immediate attention.
*
* Can be left out if the alert is not important enough to require the user's
* immediate attention.
*/
role?: "alert" | "status";
};
export const Alert = ({ type, title, message, actions, inverse }: Props) => {
export const Alert = ({
type,
title,
message,
actions,
inverse,
role,
}: Props) => {
return (
<Container type={type} inverse={inverse}>
<Container type={type} inverse={inverse} role={role}>
<Title>
{getTypeText(type)}: {title}
</Title>

View File

@@ -0,0 +1,30 @@
import { ModeledMethodValidationError } from "../../model-editor/shared/validation";
import TextButton from "../common/TextButton";
import { Alert } from "../common";
import * as React from "react";
import { useCallback } from "react";
type Props = {
error: ModeledMethodValidationError;
setSelectedIndex: (index: number) => void;
};
export const ModeledMethodAlert = ({ error, setSelectedIndex }: Props) => {
const handleClick = useCallback(() => {
setSelectedIndex(error.index);
}, [error.index, setSelectedIndex]);
return (
<Alert
role="alert"
type="error"
title={error.title}
message={
<>
{error.message}{" "}
<TextButton onClick={handleClick}>{error.actionText}</TextButton>
</>
}
/>
);
};

View File

@@ -1,11 +1,13 @@
import * as React from "react";
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { styled } from "styled-components";
import { MethodModelingInputs } from "./MethodModelingInputs";
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
import { Codicon } from "../common";
import { validateModeledMethods } from "../../model-editor/shared/validation";
import { ModeledMethodAlert } from "./ModeledMethodAlert";
export type MultipleModeledMethodsPanelProps = {
method: Method;
@@ -19,9 +21,14 @@ const Container = styled.div`
gap: 0.25rem;
padding-bottom: 0.5rem;
border-top: 0.05rem solid var(--vscode-panelSection-border);
border-bottom: 0.05rem solid var(--vscode-panelSection-border);
`;
const AlertContainer = styled.div`
margin-top: 0.5rem;
`;
const Footer = styled.div`
display: flex;
flex-direction: row;
@@ -47,8 +54,24 @@ export const MultipleModeledMethodsPanel = ({
setSelectedIndex((previousIndex) => previousIndex + 1);
}, []);
const validationErrors = useMemo(
() => validateModeledMethods(modeledMethods),
[modeledMethods],
);
return (
<Container>
{validationErrors.length > 0 && (
<AlertContainer>
{validationErrors.map((error, index) => (
<ModeledMethodAlert
key={index}
error={error}
setSelectedIndex={setSelectedIndex}
/>
))}
</AlertContainer>
)}
{modeledMethods.length > 0 ? (
<MethodModelingInputs
method={method}

View File

@@ -194,6 +194,16 @@ describe(MultipleModeledMethodsPanel.name, () => {
}),
).toHaveValue("source");
});
it("does not show errors", () => {
render({
method,
modeledMethods,
onChange,
});
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
});
describe("with three modeled methods", () => {
@@ -309,4 +319,56 @@ describe(MultipleModeledMethodsPanel.name, () => {
).toHaveValue("remote");
});
});
describe("with duplicate modeled methods", () => {
const modeledMethods = [
createModeledMethod({
...method,
}),
createModeledMethod({
...method,
}),
];
it("shows errors", () => {
render({
method,
modeledMethods,
onChange,
});
expect(screen.getByRole("alert")).toBeInTheDocument();
});
it("shows the correct error message", async () => {
render({
method,
modeledMethods,
onChange,
});
expect(
screen.getByText("Error: Duplicated classification"),
).toBeInTheDocument();
expect(
screen.getByText(
"This method has two identical or conflicting classifications.",
),
).toBeInTheDocument();
expect(screen.getByText("1/2")).toBeInTheDocument();
const button = screen.getByText(
"Modify or remove the duplicated classification.",
);
await userEvent.click(button);
expect(screen.getByText("2/2")).toBeInTheDocument();
expect(
screen.getByText("Modify or remove the duplicated classification."),
).toBeInTheDocument();
});
});
});