feat: add new form field type "boolean" and "options" field
This commit is contained in:
parent
fc7f4b8358
commit
5472b769da
7 changed files with 109 additions and 38 deletions
|
@ -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 };
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { Select, SelectItem, SelectContent, SelectTrigger, SelectValue, SelectGroup, SelectLabel, SelectSeparator } from './Select';
|
||||
export { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from './Select';
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 = () => (
|
||||
<>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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];
|
||||
|
|
Loading…
Add table
Reference in a new issue