Skip to content

Commit

Permalink
Validate the model form
Browse files Browse the repository at this point in the history
  • Loading branch information
harishmohanraj committed Apr 18, 2024
1 parent b87c97c commit f63815d
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 22 deletions.
19 changes: 19 additions & 0 deletions app/src/client/app/utils/formHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type ValidationError = {
type: string;
loc: string[];
msg: string;
input: any;
url: string;
ctx?: any;
};

type ErrorOutput = { [key: string]: string };

export function parseValidationErrors(errors: ValidationError[]): ErrorOutput {
const result: ErrorOutput = {};
errors.forEach((error) => {
const key = error.loc[error.loc.length - 1]; // Using the last item in 'loc' array as the key
result[key] = error.msg;
});
return result;
}
21 changes: 12 additions & 9 deletions app/src/client/components/DynamicFormBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { JsonSchema } from '../interfaces/models';
import { TextInput } from './form/TextInput';
import { SelectInput } from './form/SelectInput';
import { validateForm } from '../services/commonService';
import NotificationBox from '../components/NotificationBox';
import { object } from 'zod';
import { parseValidationErrors } from '../app/utils/formHelpers';
import Loader from '../admin/common/Loader';

interface DynamicFormBuilderProps {
jsonSchema: JsonSchema;
Expand All @@ -14,21 +14,19 @@ interface DynamicFormBuilderProps {
}

const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({ jsonSchema, validationURL, onSuccessCallback }) => {
const { formData, handleChange } = useForm(jsonSchema);
const [error, setError] = useState<string | null>(null);
const { formData, handleChange, formErrors, setFormErrors } = useForm(jsonSchema);
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setIsLoading(true);
try {
const response = await validateForm(formData, validationURL);
setError(null);
onSuccessCallback(response);
} catch (error: any) {
setError(error.message);
console.log(JSON.parse(error.message));
console.log(typeof JSON.parse(error.message));
const errorMsgObj = JSON.parse(error.message);
const errors = parseValidationErrors(errorMsgObj);
setFormErrors(errors);
}
setIsLoading(false);
};
Expand All @@ -55,6 +53,7 @@ const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({ jsonSchema, val
onChange={(value) => handleChange(key, value)}
/>
)}
{formErrors[key] && <div style={{ color: 'red' }}>{formErrors[key]}</div>}
</div>
)
)}
Expand All @@ -69,7 +68,11 @@ const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({ jsonSchema, val
</button>
</div>
</form>
{error && <NotificationBox type={'error'} onClick={() => setError(null)} message={error} />}
{isLoading && (
<div className='absolute inset-0 flex items-center justify-center bg-white bg-opacity-50'>
<Loader />
</div>
)}
</>
);
};
Expand Down
16 changes: 8 additions & 8 deletions app/src/client/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
// hooks/useForm.ts
import { useState, useEffect } from 'react';
import { JsonSchema } from '../interfaces/models';

export const useForm = (jsonSchema: JsonSchema) => {
const [formData, setFormData] = useState<{ [key: string]: any }>({});
const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({});

useEffect(() => {
const initialValues: { [key: string]: any } = {};
Object.keys(jsonSchema.properties).forEach((key) => {
const property = jsonSchema.properties[key];
// Check for enum with exactly one value and set it, otherwise use default or fallback to an empty string
if (property.enum && property.enum.length === 1) {
initialValues[key] = property.enum[0]; // Auto-set single enum value
initialValues[key] = property.enum[0];
} else {
initialValues[key] = property.default ?? ''; // Use default or empty string if no default
initialValues[key] = property.default ?? '';
}
});
setFormData(initialValues);
setFormErrors({}); // Reset errors on schema change
}, [jsonSchema]);

const handleChange = (key: string, value: any) => {
setFormData((prev) => ({
...prev,
[key]: value,
}));
setFormData((prev) => ({ ...prev, [key]: value }));
setFormErrors((prev) => ({ ...prev, [key]: '' })); // Clear error on change
};

return {
formData,
handleChange,
formErrors,
setFormErrors, // Expose this to allow setting errors from the component
};
};
66 changes: 61 additions & 5 deletions app/src/client/tests/DynamicFormBuilder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import DynamicFormBuilder from '../components/DynamicFormBuilder';
import { JsonSchema } from '../interfaces/models';
import { validateForm } from '../services/commonService';

const setFormErrors = vi.fn();
const handleChange = vi.fn();
vi.mock('../hooks/useForm', () => ({
useForm: () => ({
formData: {
Expand All @@ -15,7 +17,9 @@ vi.mock('../hooks/useForm', () => ({
base_url: 'https://api.openai.com/v1',
api_type: 'openai',
},
handleChange: vi.fn(),
handleChange,
formErrors: {},
setFormErrors,
}),
}));

Expand Down Expand Up @@ -92,9 +96,38 @@ describe('DynamicFormBuilder', () => {
expect(onSuccessCallback).toHaveBeenCalled();
});

test('shows an error message when submission fails', async () => {
// Mock the validateForm to simulate a failure
vi.mocked(validateForm).mockImplementationOnce(() => Promise.reject(new Error('Failed to validate')));
// test('shows an error message when submission fails', async () => {
// // Mock the validateForm to simulate a failure
// vi.mocked(validateForm).mockImplementationOnce(() => Promise.reject(new Error('Failed to validate')));
// const onSuccessCallback = vi.fn();
// renderInContext(
// <DynamicFormBuilder
// jsonSchema={jsonSchema}
// validationURL='https://some-domain/some-route'
// onSuccessCallback={onSuccessCallback}
// />
// );
// await fireEvent.submit(screen.getByRole('button', { name: /submit/i }));
// expect(onSuccessCallback).not.toHaveBeenCalled();
// });

test('displays validation errors next to form fields', async () => {
const validationErrorMessage = 'This field is required';
// Mock validateForm to simulate an API error response
vi.mocked(validateForm).mockRejectedValueOnce(
new Error(
JSON.stringify([
{
type: 'string_type',
loc: ['body', 'api_key'],
msg: validationErrorMessage,
input: 1,
url: 'https://errors.pydantic.dev/2.7/v/string_type',
},
])
)
);

const onSuccessCallback = vi.fn();
renderInContext(
<DynamicFormBuilder
Expand All @@ -103,7 +136,30 @@ describe('DynamicFormBuilder', () => {
onSuccessCallback={onSuccessCallback}
/>
);
await fireEvent.submit(screen.getByRole('button', { name: /submit/i }));

await act(async () => {
fireEvent.click(screen.getByTestId('form-submit-button'));
});

expect(onSuccessCallback).not.toHaveBeenCalled();
expect(setFormErrors).toHaveBeenCalledWith({
api_key: 'This field is required',
});
});
test('handles field changes', async () => {
renderInContext(
<DynamicFormBuilder
jsonSchema={jsonSchema}
validationURL='https://some-domain/some-route'
onSuccessCallback={vi.fn()}
/>
);

const input = screen.getByLabelText('API Key');
await act(async () => {
fireEvent.change(input, { target: { value: 'new-key' } });
});

expect(handleChange).toHaveBeenCalledWith('api_key', 'new-key');
});
});
56 changes: 56 additions & 0 deletions app/src/client/tests/formHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest';
import { parseValidationErrors } from '../app/utils/formHelpers';

describe('parseValidationErrors', () => {
it('converts a list of Pydantic validation errors to a simplified error object', () => {
const input = [
{
type: 'string_type',
loc: ['body', 'api_key'],
msg: 'Input should be a valid string',
input: 4,
url: 'https://errors.pydantic.dev/2.7/v/string_type',
},
{
type: 'url_parsing',
loc: ['body', 'base_url'],
msg: 'Input should be a valid URL, relative URL without a base',
input: '1',
ctx: {
error: 'relative URL without a base',
},
url: 'https://errors.pydantic.dev/2.7/v/url_parsing',
},
];

const expectedOutput = {
api_key: 'Input should be a valid string',
base_url: 'Input should be a valid URL, relative URL without a base',
};

const result = parseValidationErrors(input);
expect(result).toEqual(expectedOutput);
});

it('converts a list of Pydantic validation errors to a simplified error object', () => {
const input = [
{
type: 'url_parsing',
loc: ['body', 'base_url'],
msg: 'Input should be a valid URL, relative URL without a base',
input: '1',
ctx: {
error: 'relative URL without a base',
},
url: 'https://errors.pydantic.dev/2.7/v/url_parsing',
},
];

const expectedOutput = {
base_url: 'Input should be a valid URL, relative URL without a base',
};

const result = parseValidationErrors(input);
expect(result).toEqual(expectedOutput);
});
});

0 comments on commit f63815d

Please sign in to comment.