feat: add new form field type "boolean" and "options" field

This commit is contained in:
Nicolas Meienberger 2023-04-15 02:01:58 +02:00 committed by Nicolas Meienberger
parent fc7f4b8358
commit 5472b769da
7 changed files with 109 additions and 38 deletions

View file

@ -14,8 +14,6 @@ const Select: React.FC<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Roo
return <SelectPrimitive.Root {...props}>{children}</SelectPrimitive.Root>;
};
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
// Button
@ -27,7 +25,14 @@ const SelectTrigger = React.forwardRef<React.ElementRef<typeof SelectPrimitive.T
{label}
</span>
)}
<SelectPrimitive.Trigger ref={ref} className={clsx('d-flex w-100 align-items-center justify-content-between form-select', { 'is-invalid is-invalid-lite': error })} {...props}>
<SelectPrimitive.Trigger
id={props.name}
aria-label={props.name}
name={props.name}
ref={ref}
className={clsx('d-flex w-100 align-items-center justify-content-between form-select', { 'is-invalid is-invalid-lite': error })}
{...props}
>
{children}
</SelectPrimitive.Trigger>
{error && <div className="invalid-feedback">{error}</div>}
@ -51,11 +56,6 @@ const SelectContent = React.forwardRef<React.ElementRef<typeof SelectPrimitive.C
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={clsx('', className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item ref={ref} className={clsx('ps-8 position-relative d-flex align-items-center dropdown-item', className)} {...props}>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
@ -68,9 +68,4 @@ const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={clsx('', className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator };
export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem };

View file

@ -1 +1 @@
export { Select, SelectItem, SelectContent, SelectTrigger, SelectValue, SelectGroup, SelectLabel, SelectSeparator } from './Select';
export { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from './Select';

View file

@ -7,13 +7,13 @@ import classes from './Switch.module.scss';
type RootProps = typeof SwitchPrimitives.Root;
const Switch = React.forwardRef<React.ElementRef<RootProps>, React.ComponentPropsWithoutRef<RootProps> & { label?: string | React.ReactNode }>(({ className, ...props }, ref) => (
const Switch = React.forwardRef<React.ElementRef<RootProps>, React.ComponentPropsWithoutRef<RootProps> & { label?: string | React.ReactNode }>(({ className, label, ...props }, ref) => (
<label htmlFor={props.name} aria-labelledby={props.name} className={clsx('form-check form-switch form-check-sigle', className)}>
<SwitchPrimitives.Root name={props.name} className={clsx('form-check-input', classes.root)} {...props} ref={ref}>
<SwitchPrimitives.Root aria-label={props.name} className={clsx('form-check-input', classes.root)} {...props} ref={ref}>
<SwitchPrimitives.Thumb />
</SwitchPrimitives.Root>
<span id={props.name} className="form-check-label text-muted">
{props.label}
{label}
</span>
</label>
));

View file

@ -1,4 +1,5 @@
import React from 'react';
import { faker } from '@faker-js/faker';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { FormField } from '../../../../core/types';
import { InstallForm } from './InstallForm';
@ -46,7 +47,19 @@ describe('Test: InstallForm', () => {
});
it('should show validation error when required field is empty', async () => {
const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true }];
const formFields: FormField[] = [
{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true },
{
env_variable: 'test-select',
label: 'test-select',
type: 'text',
required: true,
options: [
{ label: '1', value: '1' },
{ label: '2', value: '2' },
],
},
];
const onSubmit = jest.fn();
@ -56,17 +69,36 @@ describe('Test: InstallForm', () => {
await waitFor(() => {
expect(screen.getByText('test-field is required')).toBeInTheDocument();
expect(screen.getByText('test-select is required')).toBeInTheDocument();
});
});
it('should pre-fill fields if initialValues are provided', () => {
const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true }];
const selectValue = faker.random.word();
const booleanValue = faker.datatype.boolean();
const formFields: FormField[] = [
{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true },
{
env_variable: 'test-select',
label: 'test-select',
type: 'text',
required: false,
options: [
{ label: '1', value: '1' },
{ label: 'Should appear', value: selectValue },
],
},
{ env_variable: 'test-boolean', label: 'test-boolean', type: 'boolean', required: true },
];
const onSubmit = jest.fn();
render(<InstallForm formFields={formFields} onSubmit={onSubmit} initalValues={{ 'test-env': 'test' }} />);
render(<InstallForm formFields={formFields} onSubmit={onSubmit} initalValues={{ 'test-env': 'test', 'test-select': selectValue, 'test-boolean': booleanValue }} />);
expect(screen.getByLabelText('test-field')).toHaveValue('test');
expect(screen.getByRole('textbox', { name: 'test-env' })).toHaveValue('test');
expect(screen.getByRole('combobox', { name: 'test-select' })).toHaveTextContent('Should appear');
expect(screen.getByRole('switch', { name: 'test-boolean' })).toHaveAttribute('aria-checked', booleanValue.toString());
});
it('should render expose switch when app is exposable', () => {

View file

@ -1,11 +1,14 @@
import React, { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
import { Tooltip } from 'react-tooltip';
import clsx from 'clsx';
import { Button } from '../../../../components/ui/Button';
import { Switch } from '../../../../components/ui/Switch';
import { Input } from '../../../../components/ui/Input';
import { validateAppConfig } from '../../utils/validators';
import { FormField } from '../../../../core/types';
import { type FormField } from '../../../../core/types';
interface IProps {
formFields: FormField[];
@ -44,17 +47,57 @@ export const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValu
}
}, [initalValues, isDirty, setValue]);
const renderField = (field: FormField) => (
<Input
key={field.env_variable}
{...register(field.env_variable)}
label={field.label}
error={errors[field.env_variable]?.message}
disabled={loading}
className="mb-3"
placeholder={field.hint || field.label}
/>
);
const renderField = (field: FormField) => {
const label = (
<>
{field.label}
{field.required && <span className="ms-1 text-danger">*</span>}
{Boolean(field.hint) && (
<>
<Tooltip anchorSelect={`.${field.env_variable}`}>{field.hint}</Tooltip>
<span className={clsx('ms-1 form-help', field.env_variable)}>?</span>
</>
)}
</>
);
if (field.type === 'boolean') {
return (
<Controller
control={control}
name={field.env_variable}
defaultValue={field.default}
render={({ field: { onChange, value, ref, ...props } }) => <Switch className="mb-3" ref={ref} checked={Boolean(value)} onCheckedChange={onChange} {...props} label={label} />}
/>
);
}
if (Array.isArray(field.options)) {
return (
<Controller
control={control}
name={field.env_variable}
defaultValue={field.default}
render={({ field: { onChange, value, ref, ...props } }) => (
<Select value={value as string} defaultValue={field.default as string} onValueChange={onChange} {...props}>
<SelectTrigger className="mb-3" error={errors[field.env_variable]?.message} label={label}>
<SelectValue placeholder="Choose an option..." />
</SelectTrigger>
<SelectContent>
{field.options?.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
);
}
return <Input key={field.env_variable} {...register(field.env_variable)} label={label} error={errors[field.env_variable]?.message} disabled={loading} className="mb-3" placeholder={field.label} />;
};
const renderExposeForm = () => (
<>

View file

@ -22,8 +22,8 @@ describe('InstallModal', () => {
it('renders the InstallForm with the correct props', () => {
render(<InstallModal info={app} isOpen onClose={jest.fn()} onSubmit={jest.fn()} />);
expect(screen.getByLabelText(app.form_fields[0]?.label || '')).toBeInTheDocument();
expect(screen.getByLabelText(app.form_fields[1]?.label || '')).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: app.form_fields[0]?.env_variable })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: app.form_fields[1]?.env_variable })).toBeInTheDocument();
});
it('calls onClose when the close button is clicked', () => {
@ -38,8 +38,8 @@ describe('InstallModal', () => {
const onSubmit = jest.fn();
render(<InstallModal info={app} isOpen onClose={jest.fn()} onSubmit={onSubmit} />);
const hostnameInput = screen.getByLabelText(app.form_fields[0]?.label || '');
const passwordInput = screen.getByLabelText(app.form_fields[1]?.label || '');
const hostnameInput = screen.getByRole('textbox', { name: app.form_fields[0]?.env_variable });
const passwordInput = screen.getByRole('textbox', { name: app.form_fields[1]?.env_variable });
fireEvent.change(hostnameInput, { target: { value: 'test-hostname' } });
expect(hostnameInput).toHaveValue('test-hostname');

View file

@ -41,6 +41,7 @@ export const FIELD_TYPES = {
FQDNIP: 'fqdnip',
URL: 'url',
RANDOM: 'random',
BOOLEAN: 'boolean',
} as const;
export type FieldType = (typeof FIELD_TYPES)[keyof typeof FIELD_TYPES];