import React, { useState, useReducer, useCallback } from "react"; import PropTypes from "prop-types"; import cx from "classnames"; import Form from "antd/lib/form"; import Button from "antd/lib/button"; import { includes, isFunction, filter, find, difference, isEmpty, mapValues } from "lodash"; import notification from "@/services/notification"; import Collapse from "@/components/Collapse"; import DynamicFormField, { FieldType } from "./DynamicFormField"; import getFieldLabel from "./getFieldLabel"; import helper from "./dynamicFormHelper"; import "./DynamicForm.less"; const ActionType = PropTypes.shape({ name: PropTypes.string.isRequired, callback: PropTypes.func.isRequired, type: PropTypes.string, pullRight: PropTypes.bool, disabledWhenDirty: PropTypes.bool, }); const AntdFormType = PropTypes.shape({ validateFieldsAndScroll: PropTypes.func, }); const fieldRules = ({ type, required, minLength }) => { const requiredRule = required; const minLengthRule = minLength && includes(["text", "email", "password"], type); const emailTypeRule = type === "email"; return [ requiredRule && { required, message: "This field is required." }, minLengthRule && { min: minLength, message: "This field is too short." }, emailTypeRule && { type: "email", message: "This field must be a valid email." }, ].filter(rule => rule); }; function normalizeEmptyValuesToNull(fields, values) { return mapValues(values, (value, key) => { const { initialValue } = find(fields, { name: key }) || {}; if ((initialValue === null || initialValue === undefined || initialValue === "") && value === "") { return null; } return value; }); } function DynamicFormFields({ fields, feedbackIcons, form }) { return fields.map(field => { const { name, type, initialValue, contentAfter } = field; const fieldLabel = getFieldLabel(field); const formItemProps = { name, className: "m-b-10", hasFeedback: type !== "checkbox" && type !== "file" && feedbackIcons, label: type === "checkbox" ? "" : fieldLabel, rules: fieldRules(field), valuePropName: type === "checkbox" ? "checked" : "value", initialValue, }; if (type === "file") { formItemProps.valuePropName = "data-value"; formItemProps.getValueFromEvent = e => { if (e && e.fileList[0]) { helper.getBase64(e.file).then(value => { form.setFieldsValue({ [name]: value }); }); } return undefined; }; } return ( {isFunction(contentAfter) ? contentAfter(form.getFieldValue(name)) : contentAfter} ); }); } DynamicFormFields.propTypes = { fields: PropTypes.arrayOf(FieldType), feedbackIcons: PropTypes.bool, form: AntdFormType.isRequired, }; DynamicFormFields.defaultProps = { fields: [], feedbackIcons: false, }; const reducerForActionSet = (state, action) => { if (action.inProgress) { state.add(action.actionName); } else { state.delete(action.actionName); } return new Set(state); }; function DynamicFormActions({ actions, isFormDirty }) { const [inProgressActions, setActionInProgress] = useReducer(reducerForActionSet, new Set()); const handleAction = useCallback(action => { const actionName = action.name; if (isFunction(action.callback)) { setActionInProgress({ actionName, inProgress: true }); action.callback(() => { setActionInProgress({ actionName, inProgress: false }); }); } }, []); return actions.map(action => ( )); } DynamicFormActions.propTypes = { actions: PropTypes.arrayOf(ActionType), isFormDirty: PropTypes.bool, }; DynamicFormActions.defaultProps = { actions: [], isFormDirty: false, }; export default function DynamicForm({ id, fields, actions, feedbackIcons, hideSubmitButton, defaultShowExtraFields, saveText, onSubmit, }) { const [isSubmitting, setIsSubmitting] = useState(false); const [showExtraFields, setShowExtraFields] = useState(defaultShowExtraFields); const [form] = Form.useForm(); const extraFields = filter(fields, { extra: true }); const regularFields = difference(fields, extraFields); const handleFinish = useCallback( values => { setIsSubmitting(true); values = normalizeEmptyValuesToNull(fields, values); onSubmit( values, msg => { const { setFieldsValue, getFieldsValue } = form; setIsSubmitting(false); setFieldsValue(getFieldsValue()); // reset form touched state notification.success(msg); }, msg => { setIsSubmitting(false); notification.error(msg); } ); }, [form, fields, onSubmit] ); const handleFinishFailed = useCallback( ({ errorFields }) => { form.scrollToField(errorFields[0].name); }, [form] ); return (
{!isEmpty(extraFields) && (
)} {!hideSubmitButton && ( )} ); } DynamicForm.propTypes = { id: PropTypes.string, fields: PropTypes.arrayOf(FieldType), actions: PropTypes.arrayOf(ActionType), feedbackIcons: PropTypes.bool, hideSubmitButton: PropTypes.bool, defaultShowExtraFields: PropTypes.bool, saveText: PropTypes.string, onSubmit: PropTypes.func, }; DynamicForm.defaultProps = { id: null, fields: [], actions: [], feedbackIcons: false, hideSubmitButton: false, defaultShowExtraFields: false, saveText: "Save", onSubmit: () => {}, };