Skip to content

Commit 3754109

Browse files
karanh37claudeCopilotharsh-vador
authored
feat: add FormBuilderV1 component to common components (#27075)
* feat: add FormBuilderV1 component to common components Move Collate's CoreFormBuilder (RJSF-based MUI form builder) into the OpenMetadata submodule as FormBuilderV1, including all fields, templates, and widgets. Updated external imports to use submodule-relative paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * add tests * fix lint * fix feedbacks * fix: add aria-labels to icon-only buttons in FormBuilderV1 templates Agent-Logs-Url: https://github.com/open-metadata/OpenMetadata/sessions/814ea72a-5a8b-4181-bce3-9ab07ac8571d Co-authored-by: karanh37 <33024356+karanh37@users.noreply.github.com> * fix * fix * lint * fix * lint --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Harsh Vador <58542468+harsh-vador@users.noreply.github.com>
1 parent 17738ec commit 3754109

18 files changed

Lines changed: 2149 additions & 0 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2025 Collate.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
import { FormProps } from '@rjsf/core';
15+
import { LoadingState } from 'Models';
16+
17+
export interface FormBuilderV1Props extends Omit<FormProps, 'validator'> {
18+
okText?: string;
19+
cancelText?: string;
20+
isLoading?: boolean;
21+
hideCancelButton?: boolean;
22+
status?: LoadingState;
23+
onCancel?: () => void;
24+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
* Copyright 2026 Collate.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
import {
15+
act,
16+
fireEvent,
17+
render,
18+
screen,
19+
waitFor,
20+
} from '@testing-library/react';
21+
import FormBuilderV1 from './FormBuilderV1';
22+
23+
const mockForm = jest.fn();
24+
const mockFormatFormDataForRender = jest.fn(
25+
(data: Record<string, unknown> = {}) => ({
26+
...data,
27+
formatted: true,
28+
})
29+
);
30+
31+
jest.mock('@openmetadata/ui-core-components', () => ({
32+
Button: jest.fn(
33+
({
34+
children,
35+
isDisabled,
36+
onClick,
37+
type,
38+
...props
39+
}: {
40+
children: React.ReactNode;
41+
isDisabled?: boolean;
42+
onClick?: () => void;
43+
type?: 'button' | 'submit';
44+
}) => (
45+
<button disabled={isDisabled} type={type} onClick={onClick} {...props}>
46+
{children}
47+
</button>
48+
)
49+
),
50+
}));
51+
52+
jest.mock('@rjsf/core', () => {
53+
const React = require('react');
54+
55+
const MockForm = (props: Record<string, unknown>) => {
56+
mockForm(props);
57+
58+
return React.createElement(
59+
'form',
60+
{
61+
'data-testid': 'rjsf-form',
62+
onSubmit: (event: React.FormEvent) => {
63+
event.preventDefault();
64+
(props.onSubmit as ((data: unknown) => void) | undefined)?.({
65+
formData: props.formData,
66+
});
67+
},
68+
},
69+
props.children
70+
);
71+
};
72+
73+
return {
74+
__esModule: true,
75+
default: MockForm,
76+
};
77+
});
78+
79+
jest.mock('../../../utils/JSONSchemaFormUtils', () => ({
80+
formatFormDataForRender: jest.fn((...args) =>
81+
mockFormatFormDataForRender(...args)
82+
),
83+
}));
84+
85+
jest.mock('../../../utils/formUtils', () => ({
86+
transformErrors: jest.fn(),
87+
}));
88+
89+
describe('FormBuilderV1', () => {
90+
const schema = {
91+
type: 'object' as const,
92+
properties: {
93+
name: {
94+
type: 'string' as const,
95+
},
96+
},
97+
};
98+
99+
beforeEach(() => {
100+
jest.clearAllMocks();
101+
});
102+
103+
it('renders with formatted form data and default actions', () => {
104+
render(<FormBuilderV1 formData={{ name: 'value' }} schema={schema} />);
105+
106+
expect(mockFormatFormDataForRender).toHaveBeenCalledWith({ name: 'value' });
107+
108+
const lastFormProps = mockForm.mock.calls.at(-1)![0];
109+
110+
expect(lastFormProps.formData).toEqual({
111+
name: 'value',
112+
formatted: true,
113+
});
114+
expect(screen.getByRole('button', { name: 'label.cancel' })).toBeVisible();
115+
expect(screen.getByTestId('submit-btn')).toHaveTextContent('label.submit');
116+
});
117+
118+
it('resets local form data and calls onCancel', async () => {
119+
const onCancel = jest.fn();
120+
121+
render(
122+
<FormBuilderV1
123+
formData={{ name: 'initial' }}
124+
schema={schema}
125+
onCancel={onCancel}
126+
/>
127+
);
128+
129+
act(() => {
130+
mockForm.mock.calls.at(-1)![0].onChange({
131+
formData: {
132+
name: 'changed',
133+
},
134+
});
135+
});
136+
137+
await waitFor(() => {
138+
expect(mockForm.mock.calls.at(-1)![0].formData).toEqual({
139+
name: 'changed',
140+
});
141+
});
142+
143+
fireEvent.click(screen.getByRole('button', { name: 'label.cancel' }));
144+
145+
expect(onCancel).toHaveBeenCalled();
146+
147+
await waitFor(() => {
148+
expect(mockForm.mock.calls.at(-1)![0].formData).toEqual({
149+
name: 'initial',
150+
formatted: true,
151+
});
152+
});
153+
});
154+
155+
it('forwards form changes and submit events', () => {
156+
const onChange = jest.fn();
157+
const onSubmit = jest.fn();
158+
159+
render(
160+
<FormBuilderV1
161+
formData={{ name: 'initial' }}
162+
schema={schema}
163+
onChange={onChange}
164+
onSubmit={onSubmit}
165+
/>
166+
);
167+
168+
const changeEvent = {
169+
formData: {
170+
name: 'updated',
171+
},
172+
};
173+
174+
act(() => {
175+
mockForm.mock.calls.at(-1)![0].onChange(changeEvent);
176+
});
177+
178+
expect(onChange).toHaveBeenCalledWith(changeEvent);
179+
180+
const submittedFormData = mockForm.mock.calls.at(-1)![0].formData;
181+
182+
fireEvent.submit(screen.getByTestId('rjsf-form'));
183+
184+
expect(onSubmit).toHaveBeenCalledWith({
185+
formData: submittedFormData,
186+
});
187+
});
188+
189+
it('supports custom labels and submit button states', () => {
190+
const { rerender } = render(
191+
<FormBuilderV1
192+
cancelText="Discard"
193+
formData={{}}
194+
okText="Save"
195+
schema={schema}
196+
/>
197+
);
198+
199+
expect(screen.getByRole('button', { name: 'Discard' })).toBeVisible();
200+
expect(screen.getByTestId('submit-btn')).toHaveTextContent('Save');
201+
202+
rerender(
203+
<FormBuilderV1 isLoading formData={{}} schema={schema} status="waiting" />
204+
);
205+
206+
expect(screen.getByTestId('submit-btn')).toBeDisabled();
207+
expect(screen.getByTestId('submit-btn')).toHaveTextContent(
208+
'label.submitting'
209+
);
210+
});
211+
212+
it('syncs localFormData when the formData prop changes', async () => {
213+
const { rerender } = render(
214+
<FormBuilderV1 formData={{ name: 'initial' }} schema={schema} />
215+
);
216+
217+
expect(mockForm.mock.calls.at(-1)![0].formData).toEqual({
218+
name: 'initial',
219+
formatted: true,
220+
});
221+
222+
rerender(<FormBuilderV1 formData={{ name: 'updated' }} schema={schema} />);
223+
224+
await waitFor(() => {
225+
expect(mockForm.mock.calls.at(-1)![0].formData).toEqual({
226+
name: 'updated',
227+
formatted: true,
228+
});
229+
});
230+
});
231+
232+
it('hides the cancel button when requested', () => {
233+
render(<FormBuilderV1 hideCancelButton formData={{}} schema={schema} />);
234+
235+
expect(
236+
screen.queryByRole('button', { name: 'label.cancel' })
237+
).not.toBeInTheDocument();
238+
});
239+
});

0 commit comments

Comments
 (0)