Selaa lähdekoodia

[ui-core] add formInput component for all input types (#4193)

Ram Prasad Agarwal 3 kuukautta sitten
vanhempi
commit
76dffc6a85

+ 291 - 0
desktop/core/src/desktop/js/reactComponents/FormInput/FormInput.test.tsx

@@ -0,0 +1,291 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import FormInput, { FieldType, FieldConfig } from './FormInput';
+
+interface MockContext {
+  hideField: boolean;
+}
+
+describe('FormInput Component', () => {
+  const mockOnChange = jest.fn();
+  const defaultProps = {
+    loading: false,
+    onChange: mockOnChange,
+    className: 'test-class'
+  };
+
+  beforeEach(() => {
+    mockOnChange.mockClear();
+  });
+
+  describe('INPUT field type', () => {
+    const inputField: FieldConfig = {
+      name: 'testInput',
+      type: FieldType.INPUT,
+      label: 'Test Input',
+      placeholder: 'Enter text'
+    };
+
+    it('should render input field with correct props', () => {
+      render(<FormInput field={inputField} value="test value" {...defaultProps} />);
+
+      const input = screen.getByDisplayValue('test value');
+      expect(input).toBeInTheDocument();
+      expect(input).toHaveAttribute('placeholder', 'Enter text');
+      expect(input).toHaveClass('test-class');
+    });
+
+    it('should call onChange when input value changes', () => {
+      render(<FormInput field={inputField} value="" {...defaultProps} />);
+
+      const input = screen.getByPlaceholderText('Enter text');
+      fireEvent.change(input, { target: { value: 'new value' } });
+
+      expect(mockOnChange).toHaveBeenCalledWith('testInput', 'new value');
+    });
+
+    it('should render label for input field', () => {
+      render(<FormInput field={inputField} value="" {...defaultProps} />);
+
+      expect(screen.getByText('Test Input')).toBeInTheDocument();
+    });
+
+    it('should render tooltip icon when tooltip is provided', () => {
+      const inputWithTooltip: FieldConfig = {
+        name: 'testInputTooltip',
+        type: FieldType.INPUT,
+        label: 'Input with Tooltip',
+        tooltip: 'This is an input tooltip'
+      };
+
+      render(<FormInput field={inputWithTooltip} value="" {...defaultProps} />);
+
+      expect(screen.getByRole('img', { name: /info-circle/i })).toBeInTheDocument();
+    });
+
+    it('should handle visibility conditions - render when not hidden', () => {
+      const visibilityField: FieldConfig<MockContext> = {
+        name: 'conditionalField',
+        type: FieldType.INPUT,
+        label: 'Conditional Field',
+        isHidden: (context?: MockContext) => context?.hideField === true
+      };
+
+      render(
+        <FormInput
+          field={visibilityField}
+          context={{ hideField: false }}
+          value=""
+          {...defaultProps}
+        />
+      );
+
+      expect(screen.getByDisplayValue('')).toBeInTheDocument();
+      expect(screen.getByText('Conditional Field')).toBeInTheDocument();
+    });
+
+    it('should handle visibility conditions - not render when hidden', () => {
+      const visibilityField: FieldConfig<MockContext> = {
+        name: 'conditionalField',
+        type: FieldType.INPUT,
+        label: 'Conditional Field',
+        isHidden: (context?: MockContext) => context?.hideField === true
+      };
+
+      render(
+        <FormInput
+          field={visibilityField}
+          context={{ hideField: true }}
+          value=""
+          {...defaultProps}
+        />
+      );
+
+      expect(screen.queryByText('Conditional Field')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('SELECT field type', () => {
+    const selectField: FieldConfig = {
+      name: 'testSelect',
+      type: FieldType.SELECT,
+      label: 'Test Select',
+      placeholder: 'Choose option',
+      options: [
+        { value: 'option1', label: 'Option 1' },
+        { value: 'option2', label: 'Option 2' }
+      ]
+    };
+
+    it('should render select field with options', () => {
+      render(<FormInput field={selectField} value="option1" {...defaultProps} />);
+
+      const select = screen.getByRole('combobox');
+      expect(select).toBeInTheDocument();
+      expect(screen.getByText('Option 1')).toBeInTheDocument();
+    });
+
+    it('should call onChange when select value changes', async () => {
+      const user = userEvent.setup();
+      const onChangeSpy = jest.fn();
+      render(
+        <FormInput
+          field={selectField}
+          value="option1"
+          onChange={onChangeSpy}
+          loading={false}
+          className="test-class"
+        />
+      );
+
+      const select = screen.getByRole('combobox');
+      expect(select).toBeInTheDocument();
+
+      expect(screen.getByText('Option 1')).toBeInTheDocument();
+      await user.click(select);
+
+      const option2 = screen.getByText('Option 2');
+      await user.click(option2);
+
+      expect(onChangeSpy).toHaveBeenCalledWith('testSelect', 'option2');
+    });
+
+    it('should render tooltip icon when tooltip is provided', () => {
+      const selectWithTooltip: FieldConfig = {
+        name: 'testSelectTooltip',
+        type: FieldType.SELECT,
+        label: 'Select with Tooltip',
+        tooltip: 'This is a select tooltip',
+        options: [{ value: 'option1', label: 'Option 1' }]
+      };
+
+      render(<FormInput field={selectWithTooltip} value="" {...defaultProps} />);
+
+      expect(screen.getByRole('img', { name: /info-circle/i })).toBeInTheDocument();
+    });
+  });
+
+  describe('CHECKBOX field type', () => {
+    const checkboxField: FieldConfig = {
+      name: 'testCheckbox',
+      type: FieldType.CHECKBOX,
+      label: 'Test Checkbox',
+      tooltip: 'This is a tooltip'
+    };
+
+    it('should render checkbox field', () => {
+      render(<FormInput field={checkboxField} value={true} {...defaultProps} />);
+
+      const checkbox = screen.getByRole('checkbox');
+      expect(checkbox).toBeInTheDocument();
+      expect(checkbox).toBeChecked();
+      expect(screen.getByText('Test Checkbox')).toBeInTheDocument();
+    });
+
+    it('should call onChange when checkbox is toggled', () => {
+      render(<FormInput field={checkboxField} value={false} {...defaultProps} />);
+
+      const checkbox = screen.getByRole('checkbox');
+      fireEvent.click(checkbox);
+
+      expect(mockOnChange).toHaveBeenCalledWith('testCheckbox', true);
+    });
+
+    it('should render when default value is true', () => {
+      render(<FormInput field={checkboxField} defaultValue={true} {...defaultProps} />);
+
+      const checkbox = screen.getByRole('checkbox');
+      expect(checkbox).toBeInTheDocument();
+      expect(checkbox).toBeChecked();
+    });
+
+    it('should render when default value is false', () => {
+      render(<FormInput field={checkboxField} defaultValue={false} {...defaultProps} />);
+
+      const checkbox = screen.getByRole('checkbox');
+      expect(checkbox).toBeInTheDocument();
+      expect(checkbox).not.toBeChecked();
+    });
+
+    it('should render tooltip icon when tooltip is provided', () => {
+      render(<FormInput field={checkboxField} value={false} {...defaultProps} />);
+
+      expect(screen.getByRole('img', { name: /info-circle/i })).toBeInTheDocument();
+    });
+  });
+
+  describe('RADIO field type', () => {
+    const radioField: FieldConfig = {
+      name: 'testRadio',
+      type: FieldType.RADIO,
+      label: 'Test Radio',
+      options: [
+        { value: 'radio1', label: 'Radio 1' },
+        { value: 'radio2', label: 'Radio 2' }
+      ]
+    };
+
+    it('should render radio group with options', () => {
+      render(<FormInput field={radioField} value="radio1" {...defaultProps} />);
+
+      expect(screen.getByText('Radio 1')).toBeInTheDocument();
+      expect(screen.getByText('Radio 2')).toBeInTheDocument();
+
+      const radio1 = screen.getByDisplayValue('radio1');
+      const radio2 = screen.getByDisplayValue('radio2');
+
+      expect(radio1).toBeChecked();
+      expect(radio2).not.toBeChecked();
+    });
+
+    it('should call onChange when radio option changes', () => {
+      render(<FormInput field={radioField} value="radio1" {...defaultProps} />);
+
+      const radio2 = screen.getByDisplayValue('radio2');
+      fireEvent.click(radio2);
+
+      expect(mockOnChange).toHaveBeenCalledWith('testRadio', 'radio2');
+    });
+
+    it('should render with default value', () => {
+      render(
+        <FormInput field={radioField} defaultValue="radio2" value={undefined} {...defaultProps} />
+      );
+
+      const radio2 = screen.getByDisplayValue('radio2');
+      expect(radio2).toBeChecked();
+    });
+
+    it('should render tooltip icon when tooltip is provided', () => {
+      const radioWithTooltip: FieldConfig = {
+        name: 'testRadioTooltip',
+        type: FieldType.RADIO,
+        label: 'Radio with Tooltip',
+        tooltip: 'This is a radio tooltip',
+        options: [{ value: 'radio1', label: 'Radio 1' }]
+      };
+
+      render(<FormInput field={radioWithTooltip} value="" {...defaultProps} />);
+
+      expect(screen.getByRole('img', { name: /info-circle/i })).toBeInTheDocument();
+    });
+  });
+});

+ 172 - 0
desktop/core/src/desktop/js/reactComponents/FormInput/FormInput.tsx

@@ -0,0 +1,172 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import Input from 'cuix/dist/components/Input';
+import Select from 'cuix/dist/components/Select';
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { i18nReact } from '../../utils/i18nReact';
+import { Form, Radio, Tooltip } from 'antd';
+
+export enum FieldType {
+  CHECKBOX = 'checkbox',
+  INPUT = 'input',
+  SELECT = 'select',
+  RADIO = 'radio'
+}
+
+export interface FieldOption {
+  value: string;
+  label: string;
+}
+
+export interface FieldConfig<U = unknown> {
+  name: string;
+  type: FieldType;
+  label?: string;
+  placeholder?: string;
+  tooltip?: string;
+  options?: FieldOption[];
+  isHidden?: (context?: U) => boolean;
+  style?: React.CSSProperties;
+  nested?: boolean;
+  parentField?: string;
+}
+
+interface FormInputProps<T = string, U = unknown> {
+  field: FieldConfig<U>;
+  context?: U;
+  defaultValue?: T;
+  loading: boolean;
+  value?: T;
+  className?: string;
+  onChange: (fieldId: string, value: T) => void;
+  error?: boolean;
+}
+
+const FormInput = <T, U = unknown>({
+  field,
+  context,
+  defaultValue,
+  loading,
+  value,
+  onChange,
+  className,
+  error
+}: FormInputProps<T, U>): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  // Check if field should be hidden
+  if (field.isHidden && field.isHidden(context)) {
+    return <></>;
+  }
+
+  const renderLabel = () => (
+    <>
+      {field.label && t(field.label)}
+      {field.tooltip && (
+        <Tooltip title={t(field.tooltip)}>
+          <InfoCircleOutlined style={{ marginLeft: 4 }} />
+        </Tooltip>
+      )}
+    </>
+  );
+
+  const renderField = () => {
+    switch (field.type) {
+      case FieldType.CHECKBOX:
+        return (
+          <>
+            <Input
+              type="checkbox"
+              checked={value as boolean}
+              onChange={e => onChange(field.name, e.target.checked as T)}
+              defaultChecked={value === undefined ? (defaultValue as boolean) : undefined}
+              className={className}
+              status={error ? 'error' : undefined}
+              id={field.name}
+            />
+            <label htmlFor={field.name} style={{ marginLeft: 8 }}>
+              {renderLabel()}
+            </label>
+          </>
+        );
+
+      case FieldType.INPUT:
+        return (
+          <Input
+            value={value as string}
+            onChange={e => onChange(field.name, e.target.value as T)}
+            placeholder={field.placeholder ? t(field.placeholder) : undefined}
+            className={className}
+            status={error ? 'error' : undefined}
+            id={field.name}
+          />
+        );
+
+      case FieldType.SELECT:
+        return (
+          <Select
+            value={value as string}
+            onChange={selectedValue => onChange(field.name, selectedValue as T)}
+            options={field.options?.map(option => ({
+              value: option.value,
+              label: t(option.label)
+            }))}
+            placeholder={field.placeholder ? t(field.placeholder) : undefined}
+            getPopupContainer={triggerNode => triggerNode.parentElement}
+            bordered
+            loading={loading}
+            defaultValue={defaultValue as string}
+            className={className}
+            status={error ? 'error' : undefined}
+            id={field.name}
+          />
+        );
+
+      case FieldType.RADIO:
+        return (
+          <Radio.Group
+            value={value as string}
+            defaultValue={defaultValue as string}
+            onChange={e => onChange(field.name, e.target.value as T)}
+            className={className}
+            id={field.name}
+          >
+            {field.options?.map(option => (
+              <Radio key={option.value} value={option.value}>
+                {t(option.label)}
+              </Radio>
+            ))}
+          </Radio.Group>
+        );
+
+      default:
+        return <></>;
+    }
+  };
+
+  return (
+    <Form.Item
+      label={field.type === FieldType.CHECKBOX ? undefined : renderLabel()}
+      htmlFor={field.name}
+    >
+      {renderField()}
+    </Form.Item>
+  );
+};
+
+export default FormInput;