This is an automated email from the ASF dual-hosted git repository.
diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 5392bafe28 feat(FormModal): Specialized Modal component for forms
(#32721)
5392bafe28 is described below
commit 5392bafe28760ac62cce0a7298351940c9a50501
Author: Alexandru Soare <[email protected]>
AuthorDate: Thu Mar 20 14:28:25 2025 +0200
feat(FormModal): Specialized Modal component for forms (#32721)
---
superset-frontend/src/components/Form/Form.tsx | 10 +-
.../src/components/Modal/FormModal.test.tsx | 115 +++++++++++++++++++
.../src/components/Modal/FormModal.tsx | 126 +++++++++++++++++++++
3 files changed, 250 insertions(+), 1 deletion(-)
diff --git a/superset-frontend/src/components/Form/Form.tsx
b/superset-frontend/src/components/Form/Form.tsx
index 66a9f58d7f..d827dd5871 100644
--- a/superset-frontend/src/components/Form/Form.tsx
+++ b/superset-frontend/src/components/Form/Form.tsx
@@ -29,8 +29,16 @@ const StyledForm = styled(AntdForm)`
}
`;
-export default function Form(props: FormProps) {
+function Form(props: FormProps) {
return <StyledForm {...props} />;
}
+export default Object.assign(Form, {
+ useForm: AntdForm.useForm,
+ Item: AntdForm.Item,
+ List: AntdForm.List,
+ ErrorList: AntdForm.ErrorList,
+ Provider: AntdForm.Provider,
+});
+
export type { FormProps };
diff --git a/superset-frontend/src/components/Modal/FormModal.test.tsx
b/superset-frontend/src/components/Modal/FormModal.test.tsx
new file mode 100644
index 0000000000..c061e83acd
--- /dev/null
+++ b/superset-frontend/src/components/Modal/FormModal.test.tsx
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 {
+ render,
+ fireEvent,
+ screen,
+ userEvent,
+ waitFor,
+} from 'spec/helpers/testing-library';
+import FormModal, { FormModalProps } from 'src/components/Modal/FormModal';
+import { FormItem } from 'src/components/Form';
+import { Input } from 'src/components/Input';
+
+describe('FormModal Component', () => {
+ const children = (
+ <>
+ <FormItem
+ name="name"
+ label="Name"
+ rules={[{ required: true, message: 'Name is required' }]}
+ >
+ <Input placeholder="Enter your name" aria-label="Name" />
+ </FormItem>
+ <FormItem name="email" label="Email">
+ <Input placeholder="Enter your email" aria-label="Email" />
+ </FormItem>
+ </>
+ );
+
+ const mockedProps: FormModalProps = {
+ show: true,
+ onHide: jest.fn(),
+ title: 'Test Form Modal',
+ onSave: jest.fn(),
+ formSubmitHandler: jest.fn().mockResolvedValue(undefined),
+ initialValues: { name: '', email: '' },
+ requiredFields: ['name'],
+ children,
+ };
+
+ const renderComponent = () => render(<FormModal {...mockedProps} />);
+
+ it('should render the modal with two input fields', () => {
+ renderComponent();
+
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
+ });
+
+ it('should disable Save button when required fields are empty', async () => {
+ renderComponent();
+
+ const saveButton = screen.getByTestId('form-modal-save-button');
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('should enable Save button only when the required field is filled', async
() => {
+ renderComponent();
+
+ const nameInput = screen.getByPlaceholderText('Enter your name');
+ userEvent.type(nameInput, 'Jane Doe');
+
+ await waitFor(() => {
+ expect(screen.getByTestId('form-modal-save-button')).toBeEnabled();
+ });
+ });
+
+ it('should keep Save button disabled when only the optional field is
filled', async () => {
+ renderComponent();
+
+ const emailInput = screen.getByPlaceholderText('Enter your email');
+ userEvent.type(emailInput, '[email protected]');
+
+ await waitFor(() => {
+ expect(screen.getByTestId('form-modal-save-button')).toBeDisabled();
+ });
+ });
+
+ it('should call formSubmitHandler with correct values when submitted', async
() => {
+ renderComponent();
+
+ userEvent.type(screen.getByPlaceholderText('Enter your name'), 'Jane Doe');
+ userEvent.type(
+ screen.getByPlaceholderText('Enter your email'),
+ '[email protected]',
+ );
+
+ fireEvent.click(screen.getByText('Save'));
+
+ await waitFor(() => {
+ expect(mockedProps.formSubmitHandler).toHaveBeenCalledWith({
+ name: 'Jane Doe',
+ email: '[email protected]',
+ });
+ expect(mockedProps.onSave).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/superset-frontend/src/components/Modal/FormModal.tsx
b/superset-frontend/src/components/Modal/FormModal.tsx
new file mode 100644
index 0000000000..6141f7806e
--- /dev/null
+++ b/superset-frontend/src/components/Modal/FormModal.tsx
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 Modal, { ModalProps } from 'src/components/Modal';
+import Button from 'src/components/Button';
+import { Form } from 'src/components/Form';
+import { useState, useCallback } from 'react';
+import { t } from '@superset-ui/core';
+
+export interface FormModalProps extends ModalProps {
+ initialValues: Object;
+ formSubmitHandler: (values: Object) => Promise<void>;
+ onSave: () => void;
+ requiredFields: string[];
+}
+
+function FormModal({
+ show,
+ onHide,
+ title,
+ onSave,
+ children,
+ initialValues = {},
+ formSubmitHandler,
+ bodyStyle = {},
+ requiredFields = [],
+}: FormModalProps) {
+ const [form] = Form.useForm();
+ const [isSaving, setIsSaving] = useState(false);
+ const resetForm = useCallback(() => {
+ form.resetFields();
+ setIsSaving(false);
+ }, [form]);
+ const [submitDisabled, setSubmitDisabled] = useState(true);
+
+ const handleClose = useCallback(() => {
+ resetForm();
+ onHide();
+ }, [onHide, resetForm]);
+
+ const handleSave = useCallback(() => {
+ resetForm();
+ onSave();
+ }, [onSave, resetForm]);
+
+ const handleFormSubmit = useCallback(
+ async values => {
+ try {
+ setIsSaving(true);
+ await formSubmitHandler(values);
+ handleSave();
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setIsSaving(false);
+ }
+ },
+ [formSubmitHandler, handleSave],
+ );
+
+ const onFormChange = () => {
+ const hasErrors = form.getFieldsError().some(({ errors }) =>
errors.length);
+
+ const values = form.getFieldsValue();
+ const hasEmptyRequired = requiredFields.some(field => !values[field]);
+
+ setSubmitDisabled(hasErrors || hasEmptyRequired);
+ };
+
+ return (
+ <Modal
+ show={show}
+ title={title}
+ onHide={handleClose}
+ bodyStyle={bodyStyle}
+ footer={
+ <>
+ <Button
+ buttonStyle="secondary"
+ data-test="modal-cancel-button"
+ onClick={handleClose}
+ >
+ {t('Cancel')}
+ </Button>
+ <Button
+ buttonStyle="primary"
+ htmlType="submit"
+ onClick={() => form.submit()}
+ data-test="form-modal-save-button"
+ disabled={isSaving || submitDisabled}
+ >
+ {isSaving ? t('Saving...') : t('Save')}
+ </Button>
+ </>
+ }
+ >
+ <Form
+ form={form}
+ layout="vertical"
+ onFinish={handleFormSubmit}
+ initialValues={initialValues}
+ onValuesChange={onFormChange}
+ onFieldsChange={onFormChange}
+ >
+ {typeof children === 'function' ? children(form) : children}
+ </Form>
+ </Modal>
+ );
+}
+
+export default FormModal;