Skip to content

Commit

Permalink
DEVPROD-942 Support regular expression search on configure tasks page (
Browse files Browse the repository at this point in the history
  • Loading branch information
khelif96 authored Nov 7, 2023
1 parent 0a6ac96 commit 58aab61
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { useState } from "react";
import styled from "@emotion/styled";
import IconButton from "@leafygreen-ui/icon-button";
import { palette } from "@leafygreen-ui/palette";
import Icon from "components/Icon";
import IconTooltip from "components/IconTooltip";
import TextInput from "components/TextInputWithGlyph";
import TextInput from "components/TextInputWithValidation";
import { useUpsertQueryParams } from "hooks";
import { TestStatus } from "types/history";
import { validators } from "utils";

const { validateRegexp } = validators;
const { yellow } = palette;

interface HistoryTableTestSearchProps {
onSubmit?: () => void;
Expand All @@ -19,19 +13,11 @@ interface HistoryTableTestSearchProps {
export const HistoryTableTestSearch: React.FC<HistoryTableTestSearchProps> = ({
onSubmit = () => {},
}) => {
const [input, setInput] = useState("");
const isValid = validateRegexp(input);
const handleSubmit = useUpsertQueryParams();

const handleOnChange = (value: string) => {
setInput(value);
};
const handleOnSubmit = () => {
if (isValid) {
onSubmit();
handleSubmit({ category: TestStatus.Failed, value: input });
setInput("");
}
const handleOnSubmit = (input: string) => {
onSubmit();
handleSubmit({ category: TestStatus.Failed, value: input });
};

return (
Expand All @@ -40,30 +26,11 @@ export const HistoryTableTestSearch: React.FC<HistoryTableTestSearchProps> = ({
type="search"
label="Filter by Failed Tests"
aria-label="history-table-test-search-input"
value={input}
placeholder="Search test name regex"
onChange={(e) => handleOnChange(e.target.value)}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
e.key === "Enter" && handleOnSubmit()
}
icon={
isValid ? (
<IconButton
onClick={handleOnSubmit}
aria-label="Select plus button"
>
<Icon glyph="Plus" data-cy="tuple-select-button" />
</IconButton>
) : (
<IconTooltip
glyph="Warning"
data-cy="tuple-select-warning"
fill={yellow.base}
>
Invalid Regular Expression
</IconTooltip>
)
}
validatorErrorMessage="Invalid regular expression"
onSubmit={handleOnSubmit}
validator={validateRegexp}
clearOnSubmit
/>
</ContentWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CustomStoryObj, CustomMeta } from "test_utils/types";
import TextInputWithGlyph from ".";

export default {
title: "Components/TextInput/TextInputWithGlyph",
component: TextInputWithGlyph,
} satisfies CustomMeta<typeof TextInputWithGlyph>;

Expand Down
5 changes: 3 additions & 2 deletions src/components/TextInputWithGlyph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ type TextInputWithGlyphProps = {

const TextInputWithGlyph: React.FC<TextInputWithGlyphProps> = forwardRef(
(props, ref) => {
const { icon, ...rest } = props;
const { className, icon, ...rest } = props;

return (
<TextInputWrapper>
<TextInputWrapper className={className}>
<TextInput ref={ref} {...rest} />
<IconWrapper>{icon}</IconWrapper>
</TextInputWrapper>
Expand All @@ -38,4 +38,5 @@ const IconWrapper = styled.div`
justify-content: center;
`;

export type { TextInputWithGlyphProps };
export default TextInputWithGlyph;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CustomMeta, CustomStoryObj } from "test_utils/types";
import TextInputWithValidation from ".";

export default {
title: "Components/TextInput/TextInputWithValidation",
component: TextInputWithValidation,
} satisfies CustomMeta<typeof TextInputWithValidation>;

export const Default: CustomStoryObj<typeof TextInputWithValidation> = {
render: (args) => <TextInputWithValidation {...args} />,
argTypes: {},
args: {
validator: (v) => v !== "bad",
label: "Some search field",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, screen, userEvent } from "test_utils";
import TextInputWithValidation from ".";

describe("textInputWithValidation", () => {
it("should not be able to submit with an invalid input", async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(
<TextInputWithValidation
onSubmit={onSubmit}
label="textinput"
aria-label="textinput"
validator={(v) => v.length > 5}
/>
);
const input = screen.getByRole("textbox", { name: "textinput" });
await user.type(input, "test");
expect(input).toHaveValue("test");
await user.type(input, "{enter}");
expect(onSubmit).not.toHaveBeenCalledWith("test");
});
it("should not validate without a validation function", async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(
<TextInputWithValidation
onSubmit={onSubmit}
label="textinput"
aria-label="textinput"
/>
);
const input = screen.getByRole("textbox", { name: "textinput" });
await user.type(input, "test");
await user.type(input, "{enter}");
expect(onSubmit).toHaveBeenCalledWith("test");
});
it("should call onChange only for valid inputs", async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<TextInputWithValidation
onChange={onChange}
label="textinput"
aria-label="textinput"
validator={(v) => v.length >= 5}
/>
);
const input = screen.getByRole("textbox", { name: "textinput" });
await user.type(input, "test");
expect(onChange).not.toHaveBeenCalledWith("test");
await user.type(input, "5");
expect(onChange).toHaveBeenCalledWith("test5");
});
it("clearOnSubmit should clear the input after a valid input is submitted", async () => {
const user = userEvent.setup();
const onChange = jest.fn();
const onSubmit = jest.fn();
render(
<TextInputWithValidation
onChange={onChange}
label="textinput"
aria-label="textinput"
validator={(v) => v.length >= 5}
onSubmit={onSubmit}
clearOnSubmit
/>
);
const input = screen.getByRole("textbox", { name: "textinput" });
await user.type(input, "test");
expect(onChange).not.toHaveBeenCalledWith("test");
await user.type(input, "5");
expect(onChange).toHaveBeenCalledWith("test5");
await user.type(input, "{enter}");
expect(input).toHaveValue("");
expect(onSubmit).toHaveBeenCalledWith("test5");
});
});
99 changes: 99 additions & 0 deletions src/components/TextInputWithValidation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState, forwardRef } from "react";
import IconButton from "@leafygreen-ui/icon-button";
import { palette } from "@leafygreen-ui/palette";
import Icon from "components/Icon";
import IconTooltip from "components/IconTooltip";
import TextInputWithGlyph from "components/TextInputWithGlyph";
import type { TextInputWithGlyphProps } from "components/TextInputWithGlyph";

const { yellow } = palette;
type TextInputWithValidationProps = {
/**
* `onSubmit` will be called when the user submits a new input with the enter key or the plus button
* if the input is valid
* @param value - the value of the input
* @returns void
*/
onSubmit?: (value: string) => void;
validator?: (value: string) => boolean;
/**
* `onChange` will be called when the user types into the input and the input is valid
* @param value - the value of the input
* @returns void
*/
onChange?: (value: string) => void;
validatorErrorMessage?: string;
placeholder?: string;
/**
* If true, the input will be cleared when the user submits a new input
*/
clearOnSubmit?: boolean;
} & Omit<TextInputWithGlyphProps, "icon" | "onSubmit" | "onChange">;

const TextInputWithValidation: React.FC<TextInputWithValidationProps> =
forwardRef((props, ref) => {
const {
"aria-label": ariaLabel,
clearOnSubmit = false,
label,
onChange = () => {},
onSubmit = () => {},
validator = () => true,
validatorErrorMessage = "Invalid input",
...rest
} = props;

const [input, setInput] = useState("");
const isValid = validator(input);

const handleOnSubmit = () => {
if (isValid) {
onSubmit(input);
if (clearOnSubmit) {
setInput("");
}
}
};

const handleOnChange = (value: string) => {
if (validator(value)) {
onChange(value);
}
setInput(value);
};

return (
<TextInputWithGlyph
value={input}
onChange={(e) => handleOnChange(e.target.value)}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
e.key === "Enter" && handleOnSubmit()
}
label={label}
aria-label={ariaLabel}
ref={ref}
icon={
isValid ? (
<IconButton
onClick={handleOnSubmit}
aria-label="Select plus button"
>
<Icon glyph="Plus" />
</IconButton>
) : (
<IconTooltip
glyph="Warning"
fill={yellow.base}
aria-label="validation error"
>
{validatorErrorMessage}
</IconTooltip>
)
}
{...rest}
/>
);
});

TextInputWithValidation.displayName = "TextInputWithValidation";
export default TextInputWithValidation;
4 changes: 2 additions & 2 deletions src/components/TupleSelect/TupleSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ describe("tupleSelect", () => {
expect(input).toHaveValue("bad");
expect(onSubmit).not.toHaveBeenCalled();
expect(validator).toHaveBeenLastCalledWith("bad");
expect(screen.getByDataCy("tuple-select-warning")).toBeInTheDocument();
await user.hover(screen.queryByDataCy("tuple-select-warning"));
expect(screen.getByLabelText("validation error")).toBeInTheDocument();
await user.hover(screen.getByLabelText("validation error"));
await screen.findByText(validatorErrorMessage);
});
});
46 changes: 7 additions & 39 deletions src/components/TupleSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { useState } from "react";
import styled from "@emotion/styled";
import IconButton from "@leafygreen-ui/icon-button";
import { palette } from "@leafygreen-ui/palette";
import { Select, Option } from "@leafygreen-ui/select";
import { Label } from "@leafygreen-ui/typography";
import Icon from "components/Icon";
import IconTooltip from "components/IconTooltip";
import TextInput from "components/TextInputWithGlyph";
import TextInput from "components/TextInputWithValidation";
import { size } from "constants/tokens";

const { yellow } = palette;
type option = {
value: string;
displayName: string;
Expand All @@ -30,20 +25,12 @@ const TupleSelect: React.FC<TupleSelectProps> = ({
validator = () => true,
validatorErrorMessage = "Invalid Input",
}) => {
const [input, setInput] = useState("");
const [selected, setSelected] = useState(options[0].value);
const isValid = validator(input);

const handleOnSubmit = () => {
if (isValid) {
onSubmit({ category: selected, value: input });
setInput("");
}
const handleOnSubmit = (input: string) => {
onSubmit({ category: selected, value: input });
};

const handleOnChange = (value: string) => {
setInput(value);
};
const selectedOption = options.find((o) => o.value === selected);

return (
Expand Down Expand Up @@ -73,31 +60,12 @@ const TupleSelect: React.FC<TupleSelectProps> = ({
id="filter-input"
aria-label={selectedOption.displayName}
data-cy="tuple-select-input"
value={input}
type="search"
onChange={(e) => handleOnChange(e.target.value)}
placeholder={selectedOption.placeHolderText}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
e.key === "Enter" && handleOnSubmit()
}
icon={
isValid ? (
<IconButton
onClick={handleOnSubmit}
aria-label="Select plus button"
>
<Icon glyph="Plus" data-cy="tuple-select-button" />
</IconButton>
) : (
<IconTooltip
glyph="Warning"
data-cy="tuple-select-warning"
fill={yellow.base}
>
{validatorErrorMessage}
</IconTooltip>
)
}
validator={validator}
validatorErrorMessage={validatorErrorMessage}
onSubmit={handleOnSubmit}
clearOnSubmit
/>
</InputGroup>
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ describe("tupleSelectWithRegexConditional", () => {
expect(input).toHaveValue("bad");
expect(onSubmit).not.toHaveBeenCalled();
expect(validator).toHaveBeenLastCalledWith("bad");
expect(screen.getByDataCy("tuple-select-warning")).toBeInTheDocument();
await user.hover(screen.queryByDataCy("tuple-select-warning"));
expect(screen.getByLabelText("validation error")).toBeInTheDocument();
await user.hover(screen.getByLabelText("validation error"));
await waitFor(() => {
expect(screen.getByText(validatorErrorMessage)).toBeInTheDocument();
});
Expand Down
Loading

0 comments on commit 58aab61

Please sign in to comment.