mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
🤖 Switch InputField + InputFieldWithIcon JSX components to TS, add more test coverage, fix Storybook build (#43307)
Zed + Opus 4.6; prompt: Convert the InputField JSX component to TypeScript and remove the ts-ignore directives that we no longer need after doing so. - [x] Changes file added - [x] Automated tests updated
This commit is contained in:
parent
f829170923
commit
2891904f31
74 changed files with 842 additions and 651 deletions
|
|
@ -7,6 +7,19 @@ import type { StorybookConfig } from "@storybook/react-webpack5";
|
|||
|
||||
const config: StorybookConfig = {
|
||||
webpackFinal: async (config) => {
|
||||
if (!config.resolve) {
|
||||
config.resolve = {};
|
||||
}
|
||||
if (!config.resolve.alias) {
|
||||
config.resolve.alias = {};
|
||||
}
|
||||
(config.resolve.alias as Record<string, string>)[
|
||||
"node-sql-parser"
|
||||
] = path.resolve(
|
||||
__dirname,
|
||||
"../node_modules/@sgress454/node-sql-parser/umd/sqlite.umd.js"
|
||||
);
|
||||
|
||||
config.module?.rules?.push({
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
|
|
|
|||
1
changes/input-field-to-ts
Normal file
1
changes/input-field-to-ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Moved some core UI form components from TypeScript for better predictability/reliability
|
||||
|
|
@ -6,7 +6,6 @@ import { AppContext } from "context/app";
|
|||
import CustomLink from "components/CustomLink";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
type EnrollmentType = "workProfile" | "fullyManaged";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import CustomLink from "components/CustomLink";
|
|||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
const generateUrl = (serverUrl: string, enrollSecret: string) => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
|||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TabNav from "components/TabNav";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { IEnrollSecret } from "interfaces/enroll_secret";
|
|||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
interface ISecretEditorModalProps {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ const TargetsInput = ({
|
|||
type="search"
|
||||
iconSvg="search"
|
||||
value={searchText}
|
||||
iconPosition="start"
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
onChange={setSearchText}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, { useCallback, useState } from "react";
|
|||
import validateEquality from "components/forms/validators/validate_equality";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import { IInputFieldParseTarget } from "interfaces/form_field";
|
||||
|
||||
|
|
@ -111,7 +110,7 @@ const ConfirmInviteForm = ({
|
|||
value={name}
|
||||
error={formErrors.name}
|
||||
parseTarget
|
||||
maxLength={80}
|
||||
inputOptions={{ maxLength: 80 }}
|
||||
/>
|
||||
<InputField
|
||||
label="Password"
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ const baseClass = "form-field";
|
|||
|
||||
export interface IFormFieldProps {
|
||||
children: JSX.Element;
|
||||
label: Array<any> | JSX.Element | string;
|
||||
label: React.ReactNode;
|
||||
name: string;
|
||||
helpText?: Array<any> | JSX.Element | string;
|
||||
helpText?: React.ReactNode;
|
||||
type?: string;
|
||||
error?: string;
|
||||
error?: string | null;
|
||||
className?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
labelTooltipPosition?: PlacesType;
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classnames from "classnames";
|
||||
import { noop, pick } from "lodash";
|
||||
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const baseClass = "input-field";
|
||||
|
||||
class InputField extends Component {
|
||||
static propTypes = {
|
||||
autofocus: PropTypes.bool,
|
||||
/** readOnly displays a non-editable field */
|
||||
readOnly: PropTypes.bool,
|
||||
/** disabled displays a greyed out non-editable field */
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
inputClassName: PropTypes.string, // eslint-disable-line react/forbid-prop-types
|
||||
inputWrapperClass: PropTypes.string,
|
||||
inputOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
blockAutoComplete: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
/** Returns both name and value */
|
||||
parseTarget: PropTypes.bool,
|
||||
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
labelTooltipPosition: PropTypes.string,
|
||||
helpText: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.object,
|
||||
]),
|
||||
/** Use in conjunction with type "password" and enableCopy to see eye icon to view */
|
||||
enableShowSecret: PropTypes.bool,
|
||||
enableCopy: PropTypes.bool,
|
||||
ignore1password: PropTypes.bool,
|
||||
// Accepts string or number for HTML compatibility, (e.g., step="0.1", step={0.1})
|
||||
/** Only effective on input type number */
|
||||
step: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/** Only effective on input type number */
|
||||
min: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/** Only effective on input type number */
|
||||
max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autofocus: false,
|
||||
inputWrapperClass: "",
|
||||
inputOptions: {},
|
||||
label: null,
|
||||
labelClassName: "",
|
||||
onFocus: noop,
|
||||
onBlur: noop,
|
||||
type: "text",
|
||||
blockAutoComplete: false,
|
||||
value: "",
|
||||
parseTarget: false,
|
||||
tooltip: "",
|
||||
labelTooltipPosition: undefined,
|
||||
helpText: "",
|
||||
enableCopy: false,
|
||||
enableShowSecret: false,
|
||||
ignore1password: false,
|
||||
step: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
copied: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { autofocus } = this.props;
|
||||
const { input } = this;
|
||||
|
||||
if (autofocus) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onInputChange = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { value, name } = evt.target;
|
||||
const { onChange, parseTarget } = this.props;
|
||||
|
||||
if (parseTarget) {
|
||||
// Returns both name and value
|
||||
return onChange({ value, name });
|
||||
}
|
||||
|
||||
return onChange(value);
|
||||
};
|
||||
|
||||
onToggleSecret = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({ showSecret: !this.state.showSecret });
|
||||
return false;
|
||||
};
|
||||
|
||||
onClickCopy = (e) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(this.props.value).then(() => {
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
renderShowSecretButton = () => {
|
||||
const { onToggleSecret } = this;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
className={`${baseClass}__show-secret-icon`}
|
||||
onClick={onToggleSecret}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
renderCopyButton = () => {
|
||||
const { onClickCopy } = this;
|
||||
|
||||
const copyButtonValue = <Icon name="copy" />;
|
||||
const wrapperClasses = classnames(`${baseClass}__copy-wrapper`, {
|
||||
[`${baseClass}__copy-wrapper__text-area`]: this.props.type === "textarea",
|
||||
});
|
||||
|
||||
const copiedConfirmationClasses = classnames(
|
||||
`${baseClass}__copied-confirmation`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{this.state.copied && (
|
||||
<span className={copiedConfirmationClasses}>Copied!</span>
|
||||
)}
|
||||
<Button variant="icon" onClick={onClickCopy} size="small" iconStroke>
|
||||
{copyButtonValue}
|
||||
</Button>
|
||||
{this.props.enableShowSecret && this.renderShowSecretButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
readOnly,
|
||||
disabled,
|
||||
error,
|
||||
inputClassName,
|
||||
inputOptions,
|
||||
inputWrapperClass,
|
||||
name,
|
||||
onFocus,
|
||||
onBlur,
|
||||
placeholder,
|
||||
type,
|
||||
blockAutoComplete,
|
||||
value,
|
||||
ignore1password,
|
||||
enableCopy,
|
||||
enableShowSecret,
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
} = this.props;
|
||||
|
||||
const { onInputChange } = this;
|
||||
const shouldShowPasswordClass =
|
||||
type === "password" && !this.state.showSecret;
|
||||
const inputClasses = classnames(baseClass, inputClassName, {
|
||||
[`${baseClass}--password`]: shouldShowPasswordClass,
|
||||
[`${baseClass}--read-only`]: readOnly || disabled,
|
||||
[`${baseClass}--disabled`]: disabled,
|
||||
[`${baseClass}--error`]: error,
|
||||
[`${baseClass}__textarea`]: type === "textarea",
|
||||
});
|
||||
|
||||
const inputWrapperClasses = classnames(inputWrapperClass, {
|
||||
[`input-field--read-only`]: readOnly || disabled,
|
||||
[`input-field--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const formFieldProps = pick(this.props, [
|
||||
"helpText",
|
||||
"label",
|
||||
"error",
|
||||
"name",
|
||||
"tooltip",
|
||||
"labelTooltipPosition",
|
||||
]);
|
||||
|
||||
const inputContainerClasses = classnames(`${baseClass}__input-container`, {
|
||||
"copy-enabled": enableCopy,
|
||||
});
|
||||
|
||||
if (type === "textarea") {
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="textarea"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<textarea
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
className={inputClasses}
|
||||
disabled={readOnly || disabled}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
this.input = r;
|
||||
}}
|
||||
type={type}
|
||||
{...inputOptions}
|
||||
value={value}
|
||||
/>
|
||||
{enableCopy && this.renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = this.state.showSecret ? "text" : type;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="input"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<input
|
||||
disabled={readOnly || disabled}
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
this.input = r;
|
||||
}}
|
||||
type={inputType}
|
||||
{...inputOptions}
|
||||
value={value}
|
||||
autoComplete={blockAutoComplete ? "new-password" : ""}
|
||||
data-1p-ignore={ignore1password}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
|
||||
{enableCopy && this.renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InputField;
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import React from "react";
|
||||
import InputField from ".";
|
||||
|
||||
export default {
|
||||
component: InputField,
|
||||
title: "Components/FormFields/InputField",
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["text", "password", "email", "number", "textarea"],
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
},
|
||||
label: {
|
||||
control: "text",
|
||||
},
|
||||
error: {
|
||||
control: "text",
|
||||
},
|
||||
helpText: {
|
||||
control: "text",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
readOnly: {
|
||||
control: "boolean",
|
||||
},
|
||||
autofocus: {
|
||||
control: "boolean",
|
||||
},
|
||||
enableCopy: {
|
||||
control: "boolean",
|
||||
},
|
||||
enableShowSecret: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => <InputField {...args} />;
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
name: "basic-input",
|
||||
label: "Basic Input",
|
||||
value: "",
|
||||
placeholder: "Enter text here",
|
||||
};
|
||||
|
||||
export const WithValue = Template.bind({});
|
||||
WithValue.args = {
|
||||
...Basic.args,
|
||||
value: "Sample text",
|
||||
};
|
||||
|
||||
export const WithError = Template.bind({});
|
||||
WithError.args = {
|
||||
...Basic.args,
|
||||
error: "This field is required",
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Basic.args,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const ReadOnly = Template.bind({});
|
||||
ReadOnly.args = {
|
||||
...Basic.args,
|
||||
readOnly: true,
|
||||
value: "Read-only content",
|
||||
};
|
||||
|
||||
export const WithHelpText = Template.bind({});
|
||||
WithHelpText.args = {
|
||||
...Basic.args,
|
||||
helpText: "This is some helpful information about the input field.",
|
||||
};
|
||||
|
||||
export const Password = Template.bind({});
|
||||
Password.args = {
|
||||
...Basic.args,
|
||||
type: "password",
|
||||
label: "Password",
|
||||
placeholder: "Enter your password",
|
||||
};
|
||||
|
||||
export const Textarea = Template.bind({});
|
||||
Textarea.args = {
|
||||
...Basic.args,
|
||||
type: "textarea",
|
||||
label: "Text area",
|
||||
placeholder: "Enter multiple lines of text",
|
||||
};
|
||||
|
||||
export const WithCopyEnabled = Template.bind({});
|
||||
WithCopyEnabled.args = {
|
||||
...Basic.args,
|
||||
enableCopy: true,
|
||||
value: "This text can be copied",
|
||||
};
|
||||
|
||||
export const WithCopyEnabledInput = Template.bind({});
|
||||
WithCopyEnabledInput.args = {
|
||||
...WithCopyEnabled.args,
|
||||
};
|
||||
|
||||
export const WithTooltip = Template.bind({});
|
||||
WithTooltip.args = {
|
||||
...Basic.args,
|
||||
tooltip: "This is a tooltip for the input field",
|
||||
};
|
||||
|
||||
export const AutoFocus = Template.bind({});
|
||||
AutoFocus.args = {
|
||||
...Basic.args,
|
||||
autofocus: true,
|
||||
};
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from ".";
|
||||
|
||||
import "../../../../index.scss";
|
||||
|
|
@ -20,10 +19,6 @@ const meta: Meta<typeof InputField> = {
|
|||
blockAutoComplete: { control: "boolean" },
|
||||
enableCopy: { control: "boolean" },
|
||||
enableShowSecret: { control: "boolean" },
|
||||
copyButtonPosition: {
|
||||
control: "radio",
|
||||
options: ["inside", "outside"],
|
||||
},
|
||||
labelTooltipPosition: {
|
||||
control: "select",
|
||||
options: ["top", "right", "bottom", "left"],
|
||||
|
|
@ -47,6 +42,15 @@ export const Default: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const WithValue: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "with-value-input",
|
||||
label: "Input with Value",
|
||||
value: "Sample text",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
|
|
@ -116,6 +120,15 @@ export const WithCopyButton: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const AutoFocus: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "autofocus-input",
|
||||
label: "Autofocus Input",
|
||||
autofocus: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Textarea: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithSetup } from "test/test-utils";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "./InputField";
|
||||
|
||||
describe("InputField Component", () => {
|
||||
|
|
@ -161,4 +161,208 @@ describe("InputField Component", () => {
|
|||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls onChange with { name, value } when parseTarget is true", async () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="my-field"
|
||||
parseTarget
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText(/enter text/i), "A");
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
name: "my-field",
|
||||
value: "A",
|
||||
});
|
||||
});
|
||||
|
||||
test("renders as read-only when readOnly prop is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value="read only value"
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("auto-focuses the input when autofocus is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
autofocus
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveFocus();
|
||||
});
|
||||
|
||||
test("sets autocomplete to 'new-password' when blockAutoComplete is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
blockAutoComplete
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveAttribute(
|
||||
"autocomplete",
|
||||
"new-password"
|
||||
);
|
||||
});
|
||||
|
||||
test("sets data-1p-ignore when ignore1password is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
ignore1password
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveAttribute(
|
||||
"data-1p-ignore",
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
test("renders tooltip on label hover", async () => {
|
||||
const { user } = renderWithSetup(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
tooltip="Helpful tooltip text"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.hover(screen.getByText(/test input/i));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Helpful tooltip text")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("sets step, min, and max attributes on number input", () => {
|
||||
render(
|
||||
<InputField
|
||||
value={5}
|
||||
onChange={mockOnChange}
|
||||
label="Number Input"
|
||||
placeholder="Enter number"
|
||||
name="test-number"
|
||||
type="number"
|
||||
step={0.5}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText(/enter number/i);
|
||||
expect(input).toHaveAttribute("step", "0.5");
|
||||
expect(input).toHaveAttribute("min", "0");
|
||||
expect(input).toHaveAttribute("max", "100");
|
||||
});
|
||||
|
||||
test("copies value to clipboard when copy button is clicked", async () => {
|
||||
const writeTextMock = jest.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: writeTextMock },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<InputField
|
||||
value="Copy me"
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
enableCopy
|
||||
/>
|
||||
);
|
||||
|
||||
const copyButton = screen.getByTestId("copy-icon").closest("button")!;
|
||||
await userEvent.click(copyButton);
|
||||
|
||||
expect(writeTextMock).toHaveBeenCalledWith("Copy me");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Copied!")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders show-secret eye toggle with enableCopy and enableShowSecret on password field", async () => {
|
||||
render(
|
||||
<InputField
|
||||
value="s3cret"
|
||||
onChange={mockOnChange}
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
name="test-password"
|
||||
type="password"
|
||||
enableCopy
|
||||
enableShowSecret
|
||||
/>
|
||||
);
|
||||
|
||||
// The eye icon should be present
|
||||
const eyeIcon = screen.getByTestId("eye-icon");
|
||||
expect(eyeIcon).toBeInTheDocument();
|
||||
|
||||
// Initially the input type should be password
|
||||
const input = screen.getByPlaceholderText(/enter password/i);
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
|
||||
// Click the eye toggle to reveal the secret
|
||||
const eyeButton = eyeIcon.closest("button")!;
|
||||
await userEvent.click(eyeButton);
|
||||
|
||||
// After toggling, the input type should be text
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
|
||||
// Click again to hide
|
||||
await userEvent.click(eyeButton);
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("renders copy button in textarea mode when enableCopy is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value="Textarea content"
|
||||
onChange={mockOnChange}
|
||||
label="Test Textarea"
|
||||
placeholder="Enter text"
|
||||
name="test-textarea"
|
||||
type="textarea"
|
||||
enableCopy
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("copy-icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
257
frontend/components/forms/fields/InputField/InputField.tsx
Normal file
257
frontend/components/forms/fields/InputField/InputField.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { PlacesType } from "react-tooltip-5";
|
||||
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const baseClass = "input-field";
|
||||
|
||||
export interface IInputFieldProps {
|
||||
autofocus?: boolean;
|
||||
/** readOnly displays a non-editable field */
|
||||
readOnly?: boolean;
|
||||
/** disabled displays a greyed out non-editable field */
|
||||
disabled?: boolean;
|
||||
error?: string | null;
|
||||
inputClassName?: string;
|
||||
inputWrapperClass?: string;
|
||||
inputOptions?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
name?: string;
|
||||
/**
|
||||
* Receives the field value (string) by default, or { name, value } when
|
||||
* parseTarget is true. See IInputFieldParseTarget and InputFieldOnChange
|
||||
* in interfaces/form_field.ts for caller-side typing helpers.
|
||||
*/
|
||||
onChange?: (value: any) => void;
|
||||
onBlur?: (
|
||||
evt: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => void;
|
||||
onFocus?: (
|
||||
evt: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
blockAutoComplete?: boolean;
|
||||
value?: boolean | string | number | null;
|
||||
/** Returns both name and value */
|
||||
parseTarget?: boolean;
|
||||
tooltip?: React.ReactNode;
|
||||
labelTooltipPosition?: PlacesType;
|
||||
label?: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
helpText?: React.ReactNode;
|
||||
/** Use in conjunction with type "password" and enableCopy to see eye icon to view */
|
||||
enableShowSecret?: boolean;
|
||||
enableCopy?: boolean;
|
||||
ignore1password?: boolean;
|
||||
/** Only effective on input type number */
|
||||
step?: string | number;
|
||||
/** Only effective on input type number */
|
||||
min?: string | number;
|
||||
/** Only effective on input type number */
|
||||
max?: string | number;
|
||||
}
|
||||
|
||||
const InputField = ({
|
||||
autofocus = false,
|
||||
readOnly,
|
||||
disabled,
|
||||
error,
|
||||
inputClassName,
|
||||
inputWrapperClass = "",
|
||||
inputOptions = {},
|
||||
name,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
placeholder,
|
||||
type = "text",
|
||||
blockAutoComplete = false,
|
||||
value = "",
|
||||
parseTarget = false,
|
||||
tooltip = "",
|
||||
labelTooltipPosition,
|
||||
label = null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
labelClassName: _labelClassName = "",
|
||||
helpText = "",
|
||||
enableShowSecret = false,
|
||||
enableCopy = false,
|
||||
ignore1password = false,
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
}: IInputFieldProps): JSX.Element => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autofocus && inputRef.current) {
|
||||
(inputRef.current as HTMLElement).focus();
|
||||
}
|
||||
}, [autofocus]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const target = evt.target as HTMLInputElement;
|
||||
const { value: inputValue, name: inputName } = target;
|
||||
|
||||
if (parseTarget) {
|
||||
// Returns both name and value
|
||||
return onChange?.({ value: inputValue, name: inputName });
|
||||
}
|
||||
|
||||
return onChange?.(inputValue);
|
||||
},
|
||||
[onChange, parseTarget]
|
||||
);
|
||||
|
||||
const onToggleSecret = useCallback((evt: React.MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
setShowSecret((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const onClickCopy = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(value).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const renderShowSecretButton = () => {
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
className={`${baseClass}__show-secret-icon`}
|
||||
onClick={onToggleSecret}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCopyButton = () => {
|
||||
const copyButtonValue = <Icon name="copy" />;
|
||||
const wrapperClasses = classnames(`${baseClass}__copy-wrapper`, {
|
||||
[`${baseClass}__copy-wrapper__text-area`]: type === "textarea",
|
||||
});
|
||||
|
||||
const copiedConfirmationClasses = classnames(
|
||||
`${baseClass}__copied-confirmation`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{copied && <span className={copiedConfirmationClasses}>Copied!</span>}
|
||||
<Button variant="icon" onClick={onClickCopy} size="small" iconStroke>
|
||||
{copyButtonValue}
|
||||
</Button>
|
||||
{enableShowSecret && renderShowSecretButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const shouldShowPasswordClass = type === "password" && !showSecret;
|
||||
const inputClasses = classnames(baseClass, inputClassName, {
|
||||
[`${baseClass}--password`]: shouldShowPasswordClass,
|
||||
[`${baseClass}--read-only`]: readOnly || disabled,
|
||||
[`${baseClass}--disabled`]: disabled,
|
||||
[`${baseClass}--error`]: !!error,
|
||||
[`${baseClass}__textarea`]: type === "textarea",
|
||||
});
|
||||
|
||||
const inputWrapperClasses = classnames(inputWrapperClass, {
|
||||
[`input-field--read-only`]: readOnly || disabled,
|
||||
[`input-field--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const formFieldProps = {
|
||||
helpText,
|
||||
label,
|
||||
error,
|
||||
name: name ?? "",
|
||||
tooltip,
|
||||
labelTooltipPosition,
|
||||
};
|
||||
|
||||
const inputContainerClasses = classnames(`${baseClass}__input-container`, {
|
||||
"copy-enabled": enableCopy,
|
||||
});
|
||||
|
||||
if (type === "textarea") {
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="textarea"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<textarea
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
className={inputClasses}
|
||||
disabled={readOnly || disabled}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
inputRef.current = r;
|
||||
}}
|
||||
{...(inputOptions as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
|
||||
value={value as string | number}
|
||||
/>
|
||||
{enableCopy && renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = showSecret ? "text" : type;
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps} type="input" className={inputWrapperClasses}>
|
||||
<div className={inputContainerClasses}>
|
||||
<input
|
||||
disabled={readOnly || disabled}
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
inputRef.current = r;
|
||||
}}
|
||||
type={inputType}
|
||||
{...inputOptions}
|
||||
value={value as string | number}
|
||||
autoComplete={blockAutoComplete ? "new-password" : ""}
|
||||
data-1p-ignore={ignore1password}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
{enableCopy && renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import classnames from "classnames";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { ICON_MAP } from "components/icons";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Button from "components/buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
const baseClass = "input-icon-field";
|
||||
|
||||
class InputFieldWithIcon extends InputField {
|
||||
static propTypes = {
|
||||
autofocus: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
helpText: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
||||
iconSvg: PropTypes.oneOf(Object.keys(ICON_MAP)),
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
clearButton: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
tabIndex: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
inputOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
tooltip: PropTypes.string,
|
||||
ignore1Password: PropTypes.bool,
|
||||
};
|
||||
|
||||
renderHeading = () => {
|
||||
const { error, placeholder, name, tooltip, disabled } = this.props;
|
||||
const label = this.props.label ?? placeholder;
|
||||
|
||||
const labelClasses = classnames(`${baseClass}__label`, {
|
||||
[`${baseClass}__errors`]: !!error,
|
||||
[`${baseClass}__label--disabled`]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={labelClasses}
|
||||
data-has-tooltip={!!tooltip}
|
||||
>
|
||||
{tooltip && !error ? (
|
||||
<TooltipWrapper position="bottom-start" tipContent={tooltip}>
|
||||
{label}
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
<>{error || label}</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
renderHelpText = () => {
|
||||
const { helpText } = this.props;
|
||||
|
||||
if (helpText) {
|
||||
return (
|
||||
<span className={`${baseClass}__help-text form-field__help-text`}>
|
||||
{helpText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
error,
|
||||
iconSvg,
|
||||
name,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
type,
|
||||
value,
|
||||
disabled,
|
||||
inputOptions,
|
||||
ignore1Password,
|
||||
onClick,
|
||||
onChange,
|
||||
clearButton,
|
||||
} = this.props;
|
||||
const { onInputChange, renderHelpText } = this;
|
||||
|
||||
const wrapperClasses = classnames(baseClass, "form-field");
|
||||
|
||||
const inputClasses = classnames(
|
||||
`${baseClass}__input`,
|
||||
className,
|
||||
{ "input-with-icon": !!iconSvg },
|
||||
{ [`${baseClass}__input--error`]: error },
|
||||
{ [`${baseClass}__input--password`]: type === "password" && value }
|
||||
);
|
||||
|
||||
const inputWrapperClasses = classnames(`${baseClass}__input-wrapper`, {
|
||||
[`${baseClass}__input-wrapper--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const iconClasses = classnames(
|
||||
`${baseClass}__icon`,
|
||||
{ [`${baseClass}__icon--error`]: error },
|
||||
{ [`${baseClass}__icon--active`]: value }
|
||||
);
|
||||
|
||||
const handleClear = () => {
|
||||
onChange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{this.props.label && this.renderHeading()}
|
||||
<div className={inputWrapperClasses}>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
onChange={onInputChange}
|
||||
onClick={onClick}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
this.input = r;
|
||||
}}
|
||||
tabIndex={tabIndex}
|
||||
type={type}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
{...inputOptions}
|
||||
data-1p-ignore={ignore1Password}
|
||||
/>
|
||||
{iconSvg && <Icon name={iconSvg} className={iconClasses} />}
|
||||
{clearButton && !!value && (
|
||||
<Button
|
||||
onClick={() => handleClear()}
|
||||
variant="icon"
|
||||
className={`${baseClass}__clear-button`}
|
||||
>
|
||||
<Icon name="close-filled" color="core-fleet-black" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{renderHelpText()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InputFieldWithIcon;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
|
||||
// @ts-ignore
|
||||
import InputFieldWithIcon from ".";
|
||||
|
||||
// Define metadata for the story
|
||||
|
|
@ -95,7 +94,7 @@ export const WithTooltip: Story = {
|
|||
export const WithClearButton: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
clearButton: action("clearButton"),
|
||||
clearButton: true,
|
||||
value: "example@email.com",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithSetup } from "test/test-utils";
|
||||
// @ts-ignore
|
||||
import InputFieldWithIcon from "./InputFieldWithIcon";
|
||||
|
||||
describe("InputFieldWithIcon Component", () => {
|
||||
|
|
@ -129,4 +128,166 @@ describe("InputFieldWithIcon Component", () => {
|
|||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders icon when iconSvg is provided", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
iconSvg="search"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("search-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render icon when iconSvg is not provided", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("search-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders as disabled when disabled prop is true", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("does not allow typing when disabled", async () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByPlaceholderText(/enter text/i),
|
||||
"some text"
|
||||
);
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("autofocuses the input when autofocus is true", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
autofocus
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveFocus();
|
||||
});
|
||||
|
||||
test("does not autofocus the input when autofocus is false", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).not.toHaveFocus();
|
||||
});
|
||||
|
||||
test("calls onClick when the input is clicked", async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
onClick={mockOnClick}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByPlaceholderText(/enter text/i));
|
||||
|
||||
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("sets data-1p-ignore attribute when ignore1Password is true", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
ignore1Password
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveAttribute(
|
||||
"data-1p-ignore",
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
test("does not render clear button when value is empty", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
clearButton
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies password type styling when type is password and value is present", () => {
|
||||
render(
|
||||
<InputFieldWithIcon
|
||||
value="secret"
|
||||
onChange={mockOnChange}
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
name="test-password"
|
||||
type="password"
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText(/enter password/i);
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
expect(input).toHaveClass("input-icon-field__input--password");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { ICON_MAP } from "components/icons";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "input-icon-field";
|
||||
|
||||
type IconName = keyof typeof ICON_MAP;
|
||||
|
||||
export interface IInputFieldWithIconProps {
|
||||
autofocus?: boolean;
|
||||
error?: string | null;
|
||||
helpText?: string[] | string;
|
||||
iconSvg?: IconName;
|
||||
label?: string;
|
||||
name?: string;
|
||||
onChange?: (value: any) => void;
|
||||
onClick?: (evt: React.MouseEvent<HTMLInputElement>) => void;
|
||||
clearButton?: boolean;
|
||||
placeholder?: string;
|
||||
tabIndex?: number;
|
||||
type?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
inputOptions?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
tooltip?: string;
|
||||
ignore1Password?: boolean;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const InputFieldWithIcon = ({
|
||||
autofocus = false,
|
||||
error,
|
||||
helpText,
|
||||
iconSvg,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
onClick,
|
||||
clearButton,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
type,
|
||||
className,
|
||||
disabled,
|
||||
inputOptions,
|
||||
tooltip,
|
||||
ignore1Password,
|
||||
value,
|
||||
}: IInputFieldWithIconProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autofocus && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [autofocus]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
return onChange?.(evt.target.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const renderHeading = () => {
|
||||
const labelClasses = classnames(`${baseClass}__label`, {
|
||||
[`${baseClass}__errors`]: !!error,
|
||||
[`${baseClass}__label--disabled`]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={labelClasses}
|
||||
data-has-tooltip={!!tooltip}
|
||||
>
|
||||
{tooltip && !error ? (
|
||||
<TooltipWrapper position="bottom-start" tipContent={tooltip}>
|
||||
{label}
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
<>{error || label}</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHelpText = () => {
|
||||
if (helpText) {
|
||||
return (
|
||||
<span className={`${baseClass}__help-text form-field__help-text`}>
|
||||
{helpText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const wrapperClasses = classnames(baseClass, "form-field");
|
||||
|
||||
const inputClasses = classnames(
|
||||
`${baseClass}__input`,
|
||||
className,
|
||||
{ "input-with-icon": !!iconSvg },
|
||||
{ [`${baseClass}__input--error`]: !!error },
|
||||
{ [`${baseClass}__input--password`]: !!(type === "password" && value) }
|
||||
);
|
||||
|
||||
const inputWrapperClasses = classnames(`${baseClass}__input-wrapper`, {
|
||||
[`${baseClass}__input-wrapper--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const iconClasses = classnames(
|
||||
`${baseClass}__icon`,
|
||||
{ [`${baseClass}__icon--error`]: !!error },
|
||||
{ [`${baseClass}__icon--active`]: !!value }
|
||||
);
|
||||
|
||||
const handleClear = () => {
|
||||
onChange?.("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{label && renderHeading()}
|
||||
<div className={inputWrapperClasses}>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
onChange={onInputChange}
|
||||
onClick={onClick}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
tabIndex={tabIndex}
|
||||
type={type}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
{...inputOptions}
|
||||
data-1p-ignore={ignore1Password}
|
||||
/>
|
||||
{iconSvg && <Icon name={iconSvg} className={iconClasses} />}
|
||||
{clearButton && !!value && (
|
||||
<Button
|
||||
onClick={() => handleClear()}
|
||||
variant="icon"
|
||||
className={`${baseClass}__clear-button`}
|
||||
>
|
||||
<Icon name="close-filled" color="core-fleet-black" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{renderHelpText()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputFieldWithIcon;
|
||||
|
|
@ -59,7 +59,6 @@ const SearchField = ({
|
|||
onChange={onInputChange}
|
||||
onClick={onClick}
|
||||
clearButton={clearButton}
|
||||
iconPosition="start"
|
||||
iconSvg={icon}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import Button from "components/buttons/Button";
|
|||
import { IQuery } from "interfaces/query";
|
||||
import { IScheduledQuery } from "interfaces/scheduled_query";
|
||||
import { ITarget, ITargetsAPIResponse } from "interfaces/target";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import SelectTargetsDropdown from "components/forms/fields/SelectTargetsDropdown";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { ITarget, ITargetsAPIResponse } from "interfaces/target";
|
|||
import { IEditPackFormData } from "interfaces/pack";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import BackButton from "components/BackButton";
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { IWebhookActivities } from "interfaces/webhook";
|
|||
import Modal from "components/Modal";
|
||||
import validURL from "components/forms/validators/valid_url";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import paths from "router/paths";
|
|||
import { NotificationContext } from "context/notification";
|
||||
import certificatesAPI, { ICertificate } from "services/entities/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import configAPI from "services/entities/config";
|
|||
import teamsAPI from "services/entities/teams";
|
||||
import { ApplePlatform } from "interfaces/platform";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { NotificationContext } from "context/notification";
|
|||
import configAPI from "services/entities/config";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import Button from "components/buttons/Button";
|
|||
import { ISecretPayload } from "interfaces/secrets";
|
||||
import secretsAPI from "services/entities/secrets";
|
||||
import { NotificationContext } from "context/notification";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import { validateFormData, IAddSecretModalFormValidation } from "./helpers";
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import {
|
|||
getTargetType,
|
||||
} from "pages/SoftwarePage/helpers";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import softwareAPI from "services/entities/software";
|
|||
|
||||
import Modal from "components/Modal";
|
||||
import ModalFooter from "components/ModalFooter";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import FileUploader from "components/FileUploader";
|
||||
import TabNav from "components/TabNav";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { ISoftwarePackage } from "interfaces/software";
|
|||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Editor from "components/Editor";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { IAppStoreApp } from "interfaces/software";
|
|||
|
||||
import { IInputFieldParseTarget } from "interfaces/form_field";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
@ -110,7 +109,7 @@ const SoftwareAndroidForm = ({
|
|||
<>
|
||||
<div className={`${baseClass}__form-fields`}>
|
||||
<InputField
|
||||
autoFocus
|
||||
autofocus
|
||||
label="Application ID"
|
||||
placeholder="com.android.chrome"
|
||||
helpText={
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import Modal from "components/Modal";
|
|||
import Button from "components/buttons/Button";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import Modal from "components/Modal";
|
|||
import Button from "components/buttons/Button";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import configAPI from "services/entities/config";
|
|||
import paths from "router/paths";
|
||||
import { UNCHANGED_PASSWORD_API_RESPONSE } from "utilities/constants";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo } from "react";
|
|||
|
||||
import { ICertificateAuthorityPartial } from "interfaces/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo, useState } from "react";
|
|||
|
||||
import { ICertificateAuthorityPartial } from "interfaces/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo, useState } from "react";
|
|||
|
||||
import { ICertificateAuthorityPartial } from "interfaces/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo, useState } from "react";
|
|||
|
||||
import { ICertificateAuthorityPartial } from "interfaces/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo } from "react";
|
|||
|
||||
import { ICertificateAuthorityPartial } from "interfaces/certificates";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { IConfig } from "interfaces/config";
|
|||
import { IInputFieldParseTarget } from "interfaces/form_field";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { size } from "lodash";
|
|||
import { NotificationContext } from "context/notification";
|
||||
import conditionalAccessAPI from "services/entities/conditional_access";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Modal from "components/Modal";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import configAPI from "services/entities/config";
|
|||
import conditionalAccessAPI from "services/entities/conditional_access";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Modal from "components/Modal";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import Button from "components/buttons/Button";
|
|||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import configAPI from "services/entities/config";
|
|||
import { NotificationContext } from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button/Button";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from "interfaces/integration";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, { useContext, useState, useRef } from "react";
|
|||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import BackButton from "components/BackButton";
|
||||
import MainContent from "components/MainContent";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { NotificationContext } from "context/notification";
|
|||
import { AppContext } from "context/app";
|
||||
import configAPI from "services/entities/config";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import configAPI from "services/entities/config";
|
|||
|
||||
import SettingsSection from "pages/admin/components/SettingsSection";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Radio from "components/forms/fields/Radio/Radio";
|
||||
import Slider from "components/forms/fields/Slider/Slider";
|
||||
|
|
@ -220,7 +219,7 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
|||
label="Webhook URL"
|
||||
value={formData.webhookUrl}
|
||||
onChange={onChangeWebhookUrl}
|
||||
error={!isValidWebhookUrl && "Must be a valid URL."}
|
||||
error={!isValidWebhookUrl ? "Must be a valid URL." : undefined}
|
||||
helpText={
|
||||
<>
|
||||
When the end users clicks <b>Start</b>, a JSON payload is sent
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import PageDescription from "components/PageDescription";
|
|||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import CustomLink from "components/CustomLink";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import SettingsSection from "pages/admin/components/SettingsSection";
|
|||
import PageDescription from "components/PageDescription";
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { IInputFieldParseTarget } from "interfaces/form_field";
|
|||
import SettingsSection from "pages/admin/components/SettingsSection";
|
||||
import PageDescription from "components/PageDescription";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import validHostname from "components/forms/validators/valid_hostname";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import SettingsSection from "pages/admin/components/SettingsSection";
|
|||
import PageDescription from "components/PageDescription";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import OrgLogoIcon from "components/icons/OrgLogoIcon";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import Button from "components/buttons/Button";
|
|||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import validEmail from "components/forms/validators/valid_email";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { IInputFieldParseTarget } from "interfaces/form_field";
|
|||
|
||||
import SettingsSection from "pages/admin/components/SettingsSection";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import validURL from "components/forms/validators/valid_url";
|
|||
|
||||
import Button from "components/buttons/Button";
|
||||
import DataError from "components/DataError";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Spinner from "components/Spinner";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { ITeamFormData } from "services/entities/teams";
|
|||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
const baseClass = "create-team-modal";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, { useState, useCallback, useEffect } from "react";
|
|||
import { ITeamFormData } from "services/entities/teams";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import validatePresence from "components/forms/validators/validate_presence";
|
|||
import validEmail from "components/forms/validators/valid_email";
|
||||
// @ts-ignore
|
||||
import validPassword from "components/forms/validators/valid_password";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
|
|
@ -504,7 +503,7 @@ const UserForm = ({
|
|||
placeholder="Full name"
|
||||
value={formData.name || ""}
|
||||
inputOptions={{
|
||||
maxLength: "80",
|
||||
maxLength: 80,
|
||||
}}
|
||||
ignore1password
|
||||
parseTarget
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import classnames from "classnames";
|
|||
import { getPathWithQueryParams } from "utilities/url";
|
||||
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
|
|||
|
|
@ -201,7 +201,6 @@ const SelectQueryModal = ({
|
|||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
<div className={`${baseClass}__query-selection`}>{queryList}</div>
|
||||
</>
|
||||
|
|
@ -219,7 +218,6 @@ const SelectQueryModal = ({
|
|||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__no-queries`}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useState } from "react";
|
|||
|
||||
import { IHostEndUser } from "interfaces/host";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import SidePanelPage from "components/SidePanelPage";
|
|||
import MainContent from "components/MainContent";
|
||||
import SidePanelContent from "components/SidePanelContent";
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { renderWithSetup } from "test/test-utils";
|
|||
import { screen, render } from "@testing-library/react";
|
||||
import { noop } from "lodash";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
import LabelForm from "./LabelForm";
|
||||
|
|
@ -37,7 +36,9 @@ describe("LabelForm", () => {
|
|||
onCancel={noop}
|
||||
teamName={null}
|
||||
immutableFields={[]}
|
||||
additionalFields={<InputField name="test field" label="test field" />}
|
||||
additionalFields={
|
||||
<InputField name="test field" label="test field" value="" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
|
@ -122,6 +121,13 @@ const LabelForm = ({
|
|||
setFormValidation(fullValidation);
|
||||
};
|
||||
|
||||
const handleBlur = (
|
||||
evt: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const target = evt.currentTarget as HTMLInputElement;
|
||||
onInputBlur({ name: target.name, value: target.value });
|
||||
};
|
||||
|
||||
const onSubmitForm = (evt: React.FormEvent) => {
|
||||
evt.preventDefault();
|
||||
|
||||
|
|
@ -139,7 +145,7 @@ const LabelForm = ({
|
|||
parseTarget
|
||||
name="name"
|
||||
onChange={onFormChange}
|
||||
onBlur={onInputBlur}
|
||||
onBlur={handleBlur}
|
||||
value={name}
|
||||
inputClassName={`${baseClass}__label-title`}
|
||||
label="Name"
|
||||
|
|
@ -150,7 +156,7 @@ const LabelForm = ({
|
|||
parseTarget
|
||||
name="description"
|
||||
onChange={onFormChange}
|
||||
onBlur={onInputBlur}
|
||||
onBlur={handleBlur}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__label-description`}
|
||||
label="Description"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import Modal from "components/Modal";
|
|||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import { IQuery } from "interfaces/query";
|
||||
import { IScheduledQuery } from "interfaces/scheduled_query";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import Button from "components/buttons/Button";
|
|||
import RevealButton from "components/buttons/RevealButton";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Modal from "components/Modal";
|
||||
import Icon from "components/Icon";
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import Modal from "components/Modal";
|
|||
import Slider from "components/forms/fields/Slider";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { IPolicyFormData } from "interfaces/policy";
|
|||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
} from "interfaces/team";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import { useTeamIdParam } from "hooks/useTeamIdParam";
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import {
|
|||
} from "interfaces/schedulable_query";
|
||||
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
|
|
|||
Loading…
Reference in a new issue