From b8c36bccd28c89f3c5262cc90dda88879ef33ff0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Mar 2024 12:48:16 -0500 Subject: [PATCH] Add abiltiy to edit child records as an association on insert/edit screens. --- src/qqq/components/forms/EntityForm.tsx | 411 ++++++++++++++++++++---- 1 file changed, 344 insertions(+), 67 deletions(-) diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 8289c1a..6a2134c 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -22,8 +22,10 @@ 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"; @@ -32,22 +34,24 @@ 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 {Form, Formik, useFormikContext} from "formik"; -import React, {useContext, useEffect, useReducer, useState} from "react"; -import {useLocation, useNavigate, useParams} from "react-router-dom"; -import * as Yup from "yup"; +import Modal from "@mui/material/Modal"; +import {Form, Formik, FormikErrors, FormikTouched, FormikValues, useFormikContext} from "formik"; import QContext from "QContext"; -import colors from "qqq/assets/theme/base/colors"; 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 { @@ -58,6 +62,8 @@ interface Props defaultValues: { [key: string]: string }; disabledFields: { [key: string]: boolean } | string[]; isCopy?: boolean; + onSubmitCallback?: (values: any) => void; + overrideHeading?: string } EntityForm.defaultProps = { @@ -67,7 +73,8 @@ EntityForm.defaultProps = { closeModalHandler: null, defaultValues: {}, disabledFields: {}, - isCopy: false + isCopy: false, + onSubmitCallback: null, }; function EntityForm(props: Props): JSX.Element @@ -90,10 +97,15 @@ function EntityForm(props: Props): JSX.Element 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); @@ -101,6 +113,8 @@ function EntityForm(props: Props): JSX.Element 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: // @@ -129,7 +143,131 @@ function EntityForm(props: Props): JSX.Element {} } - function getFormSection(values: any, touched: any, formFields: any, errors: any): JSX.Element + + + /******************************************************************************* + ** + *******************************************************************************/ + 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(new QRecord({values: values})) + break; + case "edit": + newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = new QRecord({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; @@ -152,13 +290,68 @@ function EntityForm(props: Props): JSX.Element if (!Object.keys(formFields).length) { - return
Loading...
; + return
Error: No form fields in section {section.name}
; } const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] - return ; + + 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); @@ -167,10 +360,16 @@ function EntityForm(props: Props): JSX.Element 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()]); + 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[]; @@ -263,6 +462,18 @@ function EntityForm(props: Props): JSX.Element } } + /////////////////////////////////////////////////// + // if an override heading was passed in, use it. // + /////////////////////////////////////////////////// + if(props.overrideHeading) + { + setFormTitle(props.overrideHeading); + if (!props.isModal) + { + setPageHeader(props.overrideHeading); + } + } + ////////////////////////////////////// // check capabilities & permissions // ////////////////////////////////////// @@ -309,27 +520,9 @@ function EntityForm(props: Props): JSX.Element const { dynamicFormFields, formValidations, - } = DynamicFormUtils.getFormData(fieldArray); + } = DynamicFormUtils.getFormData(fieldArray, disabledFields); DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues); - if(disabledFields) - { - if(Array.isArray(disabledFields)) - { - for (let i = 0; i < disabledFields.length; i++) - { - dynamicFormFields[disabledFields[i]].isEditable = false; - } - } - else - { - for (let fieldName in disabledFields) - { - dynamicFormFields[fieldName].isEditable = false; - } - } - } - ///////////////////////////////////// // group the formFields by section // ///////////////////////////////////// @@ -337,51 +530,70 @@ function EntityForm(props: Props): JSX.Element 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 || !section.fieldNames) + if (section.isHidden) { continue; } - for (let j = 0; j < section.fieldNames.length; j++) + const hasFields = section.fieldNames && section.fieldNames.length > 0; + const hasChildRecordListWidget = section.widgetName && metaData.widgets.get(section.widgetName)?.type == "childRecordList" + if(!hasFields && !hasChildRecordListWidget) { - const fieldName = section.fieldNames[j]; - const field = tableMetaData.fields.get(fieldName); + continue; + } - if(!field) + if(hasFields) + { + for (let j = 0; j < section.fieldNames.length; j++) { - console.log(`Omitting un-found field ${fieldName} from form`); + 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; } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // 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) + else { - sectionDynamicFormFields.push(dynamicFormFields[fieldName]); + dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields); } } - - 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); + 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 // ////////////////////////////////////// @@ -395,16 +607,38 @@ function EntityForm(props: Props): JSX.Element 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 = () => { /////////////////////////////////////////////////////////////////////////////////////// @@ -429,9 +663,23 @@ function EntityForm(props: Props): JSX.Element } }; + + /******************************************************************************* + ** 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 () => { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -486,6 +734,29 @@ function EntityForm(props: Props): JSX.Element } } + // todo - associations + copy might be a "bad time" + + 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 { @@ -656,7 +926,7 @@ function EntityForm(props: Props): JSX.Element t1sectionName && formFields ? ( - {getFormSection(values, touched, formFields.get(t1sectionName), errors)} + {getFormSection(t1section, values, touched, formFields.get(t1sectionName), errors, true)} ) : null @@ -665,17 +935,7 @@ function EntityForm(props: Props): JSX.Element {formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => ( - - - {section.label} - - {getSectionHelp(section)} - - - {getFormSection(values, touched, formFields.get(section.name), errors)} - - - + {renderSection(section, values, touched, formFields, errors)} )) : null} @@ -690,6 +950,23 @@ function EntityForm(props: Props): JSX.Element )} + { + showEditChildForm && + closeEditChildForm(event, reason)}> +
+ +
+
+ } +