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;

Reply via email to