/* * QQQ - Low-code Application Framework for Engineers. * Copyright (C) 2021-2022. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {Alert} from "@mui/material"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; import Grid from "@mui/material/Grid"; import Icon from "@mui/material/Icon"; import Modal from "@mui/material/Modal"; import {Form, Formik, FormikErrors, FormikTouched, FormikValues, useFormikContext} from "formik"; import QContext from "QContext"; import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import QDynamicForm from "qqq/components/forms/DynamicForm"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import MDTypography from "qqq/components/legacy/MDTypography"; import HelpContent from "qqq/components/misc/HelpContent"; import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"; import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; import React, {useContext, useEffect, useReducer, useState} from "react"; import {useLocation, useNavigate, useParams} from "react-router-dom"; import {Value} from "sass"; import * as Yup from "yup"; interface Props { id?: string; isModal: boolean; table?: QTableMetaData; closeModalHandler?: (event: object, reason: string) => void; defaultValues: { [key: string]: string }; disabledFields: { [key: string]: boolean } | string[]; isCopy?: boolean; onSubmitCallback?: (values: any) => void; overrideHeading?: string } EntityForm.defaultProps = { id: null, isModal: false, table: null, closeModalHandler: null, defaultValues: {}, disabledFields: {}, isCopy: false, onSubmitCallback: null, }; function EntityForm(props: Props): JSX.Element { const qController = Client.getInstance(); const tableNameParam = useParams().tableName; const tableName = props.table === null ? tableNameParam : props.table.name; const {accentColor} = useContext(QContext); const [formTitle, setFormTitle] = useState(""); const [validations, setValidations] = useState({}); const [initialValues, setInitialValues] = useState({} as { [key: string]: any }); const [formFields, setFormFields] = useState(null as Map); const [t1section, setT1Section] = useState(null as QTableSection); const [t1sectionName, setT1SectionName] = useState(null as string); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]); const [alertContent, setAlertContent] = useState(""); const [warningContent, setWarningContent] = useState(""); const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [metaData, setMetaData] = useState(null as QInstance); const [record, setRecord] = useState(null as QRecord); const [tableSections, setTableSections] = useState(null as QTableSection[]); const [renderedWidgetSections, setRenderedWidgetSections] = useState({} as {[name: string]: JSX.Element}); const [childListWidgetData, setChildListWidgetData] = useState({} as {[name: string]: ChildRecordListData}); const [, forceUpdate] = useReducer((x) => x + 1, 0); const [showEditChildForm, setShowEditChildForm] = useState(null as any); const [notAllowedError, setNotAllowedError] = useState(null as string); const {pageHeader, setPageHeader} = useContext(QContext); const navigate = useNavigate(); const location = useLocation(); const cardElevation = props.isModal ? 3 : 0; //////////////////////////////////////////////////////////////////// // first take defaultValues and disabledFields from props // // but, also allow them to be sent in the hash, in the format of: // // #/defaultValues={jsonName=value}/disabledFields={jsonName=any} // //////////////////////////////////////////////////////////////////// let defaultValues = props.defaultValues; let disabledFields = props.disabledFields; const hashParts = location.hash.split("/"); for (let i = 0; i < hashParts.length; i++) { try { const parts = hashParts[i].split("=") if (parts.length > 1 && parts[0] == "defaultValues") { defaultValues = JSON.parse(decodeURIComponent(parts[1])) as { [key: string]: any }; } if (parts.length > 1 && parts[0] == "disabledFields") { disabledFields = JSON.parse(decodeURIComponent(parts[1])) as { [key: string]: any }; } } catch (e) {} } /******************************************************************************* ** *******************************************************************************/ function openAddChildRecord(name: string, widgetData: any) { let defaultValues = widgetData.defaultValuesForNewChildRecords; let disabledFields = widgetData.disabledFieldsForNewChildRecords; if(!disabledFields) { disabledFields = widgetData.defaultValuesForNewChildRecords; } doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields); } /******************************************************************************* ** *******************************************************************************/ function openEditChildRecord(name: string, widgetData: any, rowIndex: number) { let defaultValues = widgetData.queryOutput.records[rowIndex].values; let disabledFields = widgetData.disabledFieldsForNewChildRecords; if(!disabledFields) { disabledFields = widgetData.defaultValuesForNewChildRecords; } doOpenEditChildForm(name, widgetData.childTableMetaData, rowIndex, defaultValues, disabledFields); } /******************************************************************************* ** *******************************************************************************/ const deleteChildRecord = (name: string, widgetData: any, rowIndex: number) => { updateChildRecordList(name, "delete", rowIndex); }; /******************************************************************************* ** *******************************************************************************/ function doOpenEditChildForm(widgetName: string, table: QTableMetaData, rowIndex: number, defaultValues: any, disabledFields: any) { const showEditChildForm: any = {}; showEditChildForm.widgetName = widgetName; showEditChildForm.table = table; showEditChildForm.rowIndex = rowIndex; showEditChildForm.defaultValues = defaultValues; showEditChildForm.disabledFields = disabledFields; setShowEditChildForm(showEditChildForm); } /******************************************************************************* ** *******************************************************************************/ const closeEditChildForm = (event: object, reason: string) => { if (reason === "backdropClick" || reason === "escapeKeyDown") { return; } setShowEditChildForm(null); }; /******************************************************************************* ** *******************************************************************************/ function submitEditChildForm(values: any) { updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values); } /******************************************************************************* ** *******************************************************************************/ async function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any) { const metaData = await qController.loadMetaData(); const widgetMetaData = metaData.widgets.get(widgetName); const newChildListWidgetData: {[name: string]: ChildRecordListData} = Object.assign({}, childListWidgetData) if(!newChildListWidgetData[widgetName].queryOutput.records) { newChildListWidgetData[widgetName].queryOutput.records = []; } switch(action) { case "insert": newChildListWidgetData[widgetName].queryOutput.records.push({values: values}) break; case "edit": newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values}; break; case "delete": newChildListWidgetData[widgetName].queryOutput.records.splice(rowIndex, 1); break; } newChildListWidgetData[widgetName].totalRows = newChildListWidgetData[widgetName].queryOutput.records.length; setChildListWidgetData(newChildListWidgetData); const newRenderedWidgetSections = Object.assign({}, renderedWidgetSections) newRenderedWidgetSections[widgetName] = getWidgetSection(widgetMetaData, newChildListWidgetData[widgetName]); setRenderedWidgetSections(newRenderedWidgetSections); forceUpdate(); setShowEditChildForm(null); } /******************************************************************************* ** render a section (full of fields) as a form *******************************************************************************/ function getFormSection(section: QTableSection, values: any, touched: any, formFields: any, errors: any, omitWrapper = false): JSX.Element { const formData: any = {}; formData.values = values; formData.touched = touched; formData.errors = errors; formData.formFields = {}; for (let i = 0; i < formFields.length; i++) { formData.formFields[formFields[i].name] = formFields[i]; if (formFields[i].possibleValueProps) { formFields[i].possibleValueProps.otherValues = formFields[i].possibleValueProps.otherValues ?? new Map(); Object.keys(formFields).forEach((otherKey) => { formFields[i].possibleValueProps.otherValues.set(otherKey, values[otherKey]); }); } } if (!Object.keys(formFields).length) { return
Error: No form fields in section {section.name}
; } const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] if(omitWrapper) { return } return {section.label} {getSectionHelp(section)} } /******************************************************************************* ** render a section as a widget *******************************************************************************/ function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element { widgetData.viewAllLink = null; widgetMetaData.showExportButton = false; return openAddChildRecord(widgetMetaData.name, widgetData)} editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)} deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)} /> } /******************************************************************************* ** render a form section *******************************************************************************/ function renderSection(section: QTableSection, values: FormikValues | Value, touched: FormikTouched | Value, formFields: Map, errors: FormikErrors | Value) { if(section.fieldNames && section.fieldNames.length > 0) { return getFormSection(section, values, touched, formFields.get(section.name), errors); } else { return renderedWidgetSections[section.widgetName] ?? Loading {section.label}... } } if (!asyncLoadInited) { setAsyncLoadInited(true); (async () => { const tableMetaData = await qController.loadTableMetaData(tableName); setTableMetaData(tableMetaData); const metaData = await qController.loadMetaData(); setMetaData(metaData); ///////////////////////////////////////////////// // define the sections, e.g., for the left-bar // ///////////////////////////////////////////////// const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) => { return section.widgetName && metaData.widgets.get(section.widgetName)?.type == "childRecordList" && metaData.widgets.get(section.widgetName)?.defaultValues?.has("manageAssociationName") }); setTableSections(tableSections); const fieldArray = [] as QFieldMetaData[]; const sortedKeys = [...tableMetaData.fields.keys()].sort(); sortedKeys.forEach((key) => { const fieldMetaData = tableMetaData.fields.get(key); fieldArray.push(fieldMetaData); }); ///////////////////////////////////////////////////////////////////////////////////////// // if doing an edit or copy, fetch the record and pre-populate the form values from it // ///////////////////////////////////////////////////////////////////////////////////////// let record: QRecord = null; let defaultDisplayValues = new Map(); if (props.id !== null) { record = await qController.get(tableName, props.id); setRecord(record); const titleVerb = props.isCopy ? "Copy" : "Edit"; setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`); if (!props.isModal) { setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`); } tableMetaData.fields.forEach((fieldMetaData, key) => { if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField) { return; } initialValues[key] = record.values.get(key); }); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // these checks are only for updating records, if copying, it is actually an insert, which is checked after this block // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(! props.isCopy) { if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) { setNotAllowedError("Records may not be edited in this table"); } else if (!tableMetaData.editPermission) { setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`); } } } else { /////////////////////////////////////////// // else handle preparing to do an insert // /////////////////////////////////////////// setFormTitle(`Creating New ${tableMetaData?.label}`); if (!props.isModal) { setPageHeader(`Creating New ${tableMetaData?.label}`); } //////////////////////////////////////////////////////////////////////////////////////////////// // if default values were supplied for a new record, then populate initialValues, for formik. // //////////////////////////////////////////////////////////////////////////////////////////////// for (let i = 0; i < fieldArray.length; i++) { const fieldMetaData = fieldArray[i]; const fieldName = fieldMetaData.name; const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue; if (defaultValue) { initialValues[fieldName] = defaultValue; /////////////////////////////////////////////////////////////////////////////////////////// // we need to set the initialDisplayValue for possible value fields with a default value // // so, look them up here now if needed // /////////////////////////////////////////////////////////////////////////////////////////// if (fieldMetaData.possibleValueSourceName) { const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); if (results && results.length > 0) { defaultDisplayValues.set(fieldName, results[0].label); } } } } } /////////////////////////////////////////////////// // if an override heading was passed in, use it. // /////////////////////////////////////////////////// if(props.overrideHeading) { setFormTitle(props.overrideHeading); if (!props.isModal) { setPageHeader(props.overrideHeading); } } ////////////////////////////////////// // check capabilities & permissions // ////////////////////////////////////// if (props.isCopy || !props.id) { if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT)) { setNotAllowedError("Records may not be created in this table"); } else if (!tableMetaData.insertPermission) { setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`); } } else { if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) { setNotAllowedError("Records may not be edited in this table"); } else if (!tableMetaData.editPermission) { setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`); } } ///////////////////////////////////////////////////////////////////// // make sure all initialValues are properly formatted for the form // ///////////////////////////////////////////////////////////////////// for (let i = 0; i < fieldArray.length; i++) { const fieldMetaData = fieldArray[i]; if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name]) { initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]); } } setInitialValues(initialValues); ///////////////////////////////////////////////////////// // get formField and formValidation objects for Formik // ///////////////////////////////////////////////////////// const { dynamicFormFields, formValidations, } = DynamicFormUtils.getFormData(fieldArray, disabledFields); DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues); ///////////////////////////////////// // group the formFields by section // ///////////////////////////////////// const dynamicFormFieldsBySection = new Map(); let t1sectionName; let t1section; const nonT1Sections: QTableSection[] = []; const newRenderedWidgetSections: {[name: string]: JSX.Element} = {}; const newChildListWidgetData: {[name: string]: ChildRecordListData} = {}; for (let i = 0; i < tableSections.length; i++) { const section = tableSections[i]; const sectionDynamicFormFields: any[] = []; if (section.isHidden) { continue; } const hasFields = section.fieldNames && section.fieldNames.length > 0; const hasChildRecordListWidget = section.widgetName && metaData.widgets.get(section.widgetName)?.type == "childRecordList" if(!hasFields && !hasChildRecordListWidget) { continue; } if(hasFields) { for (let j = 0; j < section.fieldNames.length; j++) { const fieldName = section.fieldNames[j]; const field = tableMetaData.fields.get(fieldName); if (!field) { console.log(`Omitting un-found field ${fieldName} from form`); continue; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. // // || (or) we're on the insert screen in which case, only show editable fields. // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if ((props.id !== null && !props.isCopy) || field.isEditable) { sectionDynamicFormFields.push(dynamicFormFields[fieldName]); } } if (sectionDynamicFormFields.length === 0) { //////////////////////////////////////////////////////////////////////////////////////////////// // in case there are no active fields in this section, remove it from the tableSections array // //////////////////////////////////////////////////////////////////////////////////////////////// tableSections.splice(i, 1); i--; continue; } else { dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields); } } else { const widgetMetaData = metaData.widgets.get(section.widgetName); const widgetData = await qController.widget(widgetMetaData.name, props.id ? `${tableMetaData.primaryKeyField}=${props.id}` : ""); newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData); newChildListWidgetData[section.widgetName] = widgetData; } ////////////////////////////////////// // capture the tier1 section's name // ////////////////////////////////////// if (section.tier === "T1") { t1sectionName = section.name; t1section = section; } else { nonT1Sections.push(section); } } setT1SectionName(t1sectionName); setT1Section(t1section); setNonT1Sections(nonT1Sections); setFormFields(dynamicFormFieldsBySection); setValidations(Yup.object().shape(formValidations)); setRenderedWidgetSections(newRenderedWidgetSections); setChildListWidgetData(newChildListWidgetData); forceUpdate(); })(); } ////////////////////////////////////////////////////////////////// // watch widget data - if they change, re-render those sections // ////////////////////////////////////////////////////////////////// useEffect(() => { if(childListWidgetData) { const newRenderedWidgetSections: {[name: string]: JSX.Element} = {}; for(let name in childListWidgetData) { const widgetMetaData = metaData.widgets.get(name); newRenderedWidgetSections[name] = getWidgetSection(widgetMetaData, childListWidgetData[name]); } setRenderedWidgetSections(newRenderedWidgetSections); } }, [childListWidgetData]); const handleCancelClicked = () => { /////////////////////////////////////////////////////////////////////////////////////// // todo - we might have rather just done a navigate(-1) (to keep history clean) // // but if the user used the anchors on the page, this doesn't effectively cancel... // // what we have here pushed a new history entry (I think?), so could be better // /////////////////////////////////////////////////////////////////////////////////////// if (props.id !== null && props.isCopy) { const path = `${location.pathname.replace(/\/copy$/, "")}`; navigate(path, {replace: true}); } else if (props.id !== null) { const path = `${location.pathname.replace(/\/edit$/, "")}`; navigate(path, {replace: true}); } else { const path = `${location.pathname.replace(/\/create$/, "")}`; navigate(path, {replace: true}); } }; /******************************************************************************* ** event handler for the (Formik) Form. *******************************************************************************/ const handleSubmit = async (values: any, actions: any) => { actions.setSubmitting(true); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there anre return. // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(props.onSubmitCallback) { props.onSubmitCallback(values); return; } await (async () => { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // we will be manipulating the values sent to the backend, so clone values so they remained unchanged for the form widgets // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const valuesToPost = JSON.parse(JSON.stringify(values)); for(let fieldName of tableMetaData.fields.keys()) { const fieldMetaData = tableMetaData.fields.get(fieldName); ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // (1) convert date-time fields from user's time-zone into UTC // // (2) if there's an initial value which matches the value (e.g., from the form), then remove that field // // from the set of values that we'll submit to the backend. This is to deal with the fact that our // // date-times in the UI (e.g., the form field) only go to the minute - so they kinda always end up // // changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the // // user didn't change the value in the field (but if the user did change the value, then we will submit it) // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName]) { console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`) if (initialValues[fieldName] == valuesToPost[fieldName]) { console.log(" - Is the same, so, deleting from the post"); delete (valuesToPost[fieldName]); } else { valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // for BLOB fields, there are 3 possible cases: // // 1) they are a File object - in which case, cool, send them through to the backend to have bytes stored. // // 2) they are null - in which case, cool, send them through to the backend to be set to null. // // 3) they are a String, which is their URL path to download them... in that case, don't submit them to // // the backend at all, so they'll stay what they were. do that by deleting them from the values object here. // //////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(fieldMetaData.type === QFieldType.BLOB) { if(typeof valuesToPost[fieldName] === "string") { console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`); delete(valuesToPost[fieldName]); } else { valuesToPost[fieldName] = values[fieldName]; } } } const associationsToPost: any = {} let haveAssociationsToPost = false; for (let name of Object.keys(childListWidgetData)) { const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName") if(!manageAssociationName) { console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`); } associationsToPost[manageAssociationName] = []; haveAssociationsToPost = true; for(let i=0; i { if (props.isModal) { props.closeModalHandler(null, "recordUpdated"); } else { let warningMessage = null; if(record.warnings && record.warnings.length && record.warnings.length > 0) { warningMessage = record.warnings[0]; } const path = location.pathname.replace(/\/edit$/, ""); navigate(path, {state: {updateSuccess: true, warning: warningMessage}}); } }) .catch((error) => { console.log("Caught:"); console.log(error); if(error.message.toLowerCase().startsWith("warning")) { const path = location.pathname.replace(/\/edit$/, ""); navigate(path, {state: {updateSuccess: true, warning: error.message}}); } else { setAlertContent(error.message); HtmlUtils.autoScroll(0); } }); } else { ///////////////////////////////// // perform an insert // // todo - audit if it's a dupe // ///////////////////////////////// await qController .create(tableName, valuesToPost) .then((record) => { if (props.isModal) { props.closeModalHandler(null, "recordCreated"); } else { let warningMessage = null; if(record.warnings && record.warnings.length && record.warnings.length > 0) { warningMessage = record.warnings[0]; } const path = props.isCopy ? location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField)) : location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField)); navigate(path, {state: {createSuccess: true, warning: warningMessage}}); } }) .catch((error) => { if(error.message.toLowerCase().startsWith("warning")) { const path = props.isCopy ? location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField)) : location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField)); navigate(path, {state: {createSuccess: true, warning: error.message}}); } else { setAlertContent(error.message); HtmlUtils.autoScroll(0); } }); } })(); }; const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; let body; const getSectionHelp = (section: QTableSection) => { const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] const formattedHelpContent = ; return formattedHelpContent && ( {formattedHelpContent} ) } if (notAllowedError) { body = ( {notAllowedError} {props.isModal && } ); } else { body = ( { (alertContent || warningContent) && {alertContent ? ( setAlertContent(null)}>{alertContent} ) : ("")} {warningContent ? ( setWarningContent(null)}>{warningContent} ) : ("")} } { !props.isModal && } {({ values, errors, touched, isSubmitting, }) => (
{tableMetaData?.iconName} {formTitle} {t1section && getSectionHelp(t1section)} { t1sectionName && formFields ? ( {getFormSection(t1section, values, touched, formFields.get(t1sectionName), errors, true)} ) : null } {formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => ( {renderSection(section, values, touched, formFields, errors)} )) : null} )}
{ showEditChildForm && closeEditChildForm(event, reason)}>
}
); } if (props.isModal) { return ( {body} ); } else { return (body); } } function ScrollToFirstError(): JSX.Element { const {submitCount, isValid} = useFormikContext() useEffect(() => { ///////////////////////////////////////////////////////////////////////////// // Wrap the code in setTimeout to make sure it runs after the DOM has been // // updated and has the error message elements. // ///////////////////////////////////////////////////////////////////////////// setTimeout(() => { //////////////////////////////////////// // Only run on submit or if not valid // //////////////////////////////////////// if (submitCount === 0 || isValid) { return; } ////////////////////////////////// // Find the first error message // ////////////////////////////////// const errorMessageSelector = "[data-field-error]"; const firstErrorMessage = document.querySelector(errorMessageSelector); if (!firstErrorMessage) { console.warn(`Form failed validation but no error field was found with selector: ${errorMessageSelector}`); return; } firstErrorMessage.scrollIntoView({block: "center"}); }, 100) }, [submitCount, isValid]) return null; } export default EntityForm;