From 67feb95c60ac0b2b2589b1e5b0ec9c964da2e1eb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Mar 2024 12:48:16 -0500 Subject: [PATCH 1/6] Add abiltiy to edit child records as an association on insert/edit screens. --- package.json | 2 +- src/qqq/components/forms/DynamicFormUtils.ts | 71 ++++++++++++-- src/qqq/components/widgets/Widget.tsx | 6 +- .../widgets/misc/RecordGridWidget.tsx | 73 ++++++++++++-- src/qqq/utils/DataGridUtils.tsx | 13 ++- src/qqq/utils/qqq/TableUtils.ts | 96 +++++++++++++------ 6 files changed, 205 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 7480969..d6a2636 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.88", + "@kingsrook/qqq-frontend-core": "file:.yalc/@kingsrook/qqq-frontend-core", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/forms/DynamicFormUtils.ts b/src/qqq/components/forms/DynamicFormUtils.ts index 93ea346..3dc736f 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -24,27 +24,38 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import * as Yup from "yup"; + +type DisabledFields = { [fieldName: string]: boolean } | string[]; + /******************************************************************************* ** Meta-data to represent a single field in a table. ** *******************************************************************************/ class DynamicFormUtils { - public static getFormData(qqqFormFields: QFieldMetaData[]) + + /******************************************************************************* + ** + *******************************************************************************/ + public static getFormData(qqqFormFields: QFieldMetaData[], disabledFields?: DisabledFields) { const dynamicFormFields: any = {}; const formValidations: any = {}; qqqFormFields.forEach((field) => { - dynamicFormFields[field.name] = this.getDynamicField(field); - formValidations[field.name] = this.getValidationForField(field); + dynamicFormFields[field.name] = this.getDynamicField(field, disabledFields); + formValidations[field.name] = this.getValidationForField(field, disabledFields); }); return {dynamicFormFields, formValidations}; } - public static getDynamicField(field: QFieldMetaData) + + /******************************************************************************* + ** + *******************************************************************************/ + public static getDynamicField(field: QFieldMetaData, disabledFields?: DisabledFields) { let fieldType: string; switch (field.type.toString()) @@ -85,15 +96,21 @@ class DynamicFormUtils } } + //////////////////////////////////////////////////////////// + // this feels right, but... might be cases where it isn't // + //////////////////////////////////////////////////////////// + const effectiveIsEditable = field.isEditable && !this.isFieldDynamicallyDisabled(field.name, disabledFields); + const effectivelyIsRequired = field.isRequired && effectiveIsEditable; + let label = field.label ? field.label : field.name; - label += field.isRequired ? " *" : ""; + label += effectivelyIsRequired ? " *" : ""; return ({ fieldMetaData: field, name: field.name, label: label, - isRequired: field.isRequired, - isEditable: field.isEditable, + isRequired: effectivelyIsRequired, + isEditable: effectiveIsEditable, type: fieldType, displayFormat: field.displayFormat, // todo invalidMsg: "Zipcode is not valid (e.g. 70000).", @@ -101,11 +118,18 @@ class DynamicFormUtils }); } - public static getValidationForField(field: QFieldMetaData) + + /******************************************************************************* + ** + *******************************************************************************/ + public static getValidationForField(field: QFieldMetaData, disabledFields?: DisabledFields) { - if (field.isRequired) + const effectiveIsEditable = field.isEditable && !this.isFieldDynamicallyDisabled(field.name, disabledFields); + const effectivelyIsRequired = field.isRequired && effectiveIsEditable; + + if (effectivelyIsRequired) { - if(field.possibleValueSourceName) + if (field.possibleValueSourceName) { //////////////////////////////////////////////////////////////////////////////////////////// // the "nullable(true)" here doesn't mean that you're allowed to set the field to null... // @@ -121,6 +145,10 @@ class DynamicFormUtils return (null); } + + /******************************************************************************* + ** + *******************************************************************************/ public static addPossibleValueProps(dynamicFormFields: any, qFields: QFieldMetaData[], tableName: string, processName: string, displayValues: Map) { for (let i = 0; i < qFields.length; i++) @@ -159,6 +187,29 @@ class DynamicFormUtils } } } + + + /******************************************************************************* + ** private helper - check the disabled fields object (array or map), and return + ** true iff fieldName is in it. + *******************************************************************************/ + private static isFieldDynamicallyDisabled(fieldName: string, disabledFields?: DisabledFields): boolean + { + if (!disabledFields) + { + return (false); + } + + if (Array.isArray(disabledFields)) + { + return (disabledFields.indexOf(fieldName) > -1) + } + else + { + return (disabledFields[fieldName]); + } + } + } export default DynamicFormUtils; diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index a16661f..b421228 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -169,15 +169,17 @@ export class AddNewRecordButton extends LabelComponent label: string; defaultValues: any; disabledFields: any; + addNewRecordCallback?: () => void; - constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues) + constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues, addNewRecordCallback?: () => void) { super(); this.table = table; this.label = label; this.defaultValues = defaultValues; this.disabledFields = disabledFields; + this.addNewRecordCallback = addNewRecordCallback; } openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) => @@ -189,7 +191,7 @@ export class AddNewRecordButton extends LabelComponent { return ( - + ); }; diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 602a26d..4dc62c8 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -25,28 +25,53 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Icon from "@mui/material/Icon"; +import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip/Tooltip"; import Typography from "@mui/material/Typography"; -import {DataGridPro, GridCallbackDetails, GridEventListener, GridFilterModel, gridPreferencePanelStateSelector, GridRowParams, GridSelectionModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; -import React, {useEffect, useRef, useState} from "react"; -import {useNavigate, Link} from "react-router-dom"; -import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget"; +import {DataGridPro, GridCallbackDetails, GridEventListener, GridRenderCellParams, GridRowParams, GridToolbarContainer, MuiEvent, useGridApiContext, useGridApiEventHandler} from "@mui/x-data-grid-pro"; +import Widget, {AddNewRecordButton, LabelComponent, WidgetData} from "qqq/components/widgets/Widget"; import DataGridUtils from "qqq/utils/DataGridUtils"; import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React, {useEffect, useRef, useState} from "react"; +import {Link, useNavigate} from "react-router-dom"; + +export interface ChildRecordListData extends WidgetData +{ + title: string; + queryOutput: {records: QRecord[]} + childTableMetaData: QTableMetaData; + tablePath: string; + viewAllLink: string; + totalRows: number; + canAddChildRecord: boolean; + defaultValuesForNewChildRecords: {[fieldName: string]: any}; + disabledFieldsForNewChildRecords: {[fieldName: string]: any}; +} interface Props { widgetMetaData: QWidgetMetaData; - data: any; + data: ChildRecordListData; + addNewRecordCallback?: () => void; + disableRowClick: boolean; + allowRecordEdit: boolean; + editRecordCallback?: (rowIndex: number) => void; + allowRecordDelete: boolean; + deleteRecordCallback?: (rowIndex: number) => void; } -RecordGridWidget.defaultProps = {}; +RecordGridWidget.defaultProps = + { + disableRowClick: false, + allowRecordEdit: false, + allowRecordDelete: false + }; const qController = Client.getInstance(); -function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element +function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback}: Props): JSX.Element { const instance = useRef({timer: null}); const [rows, setRows] = useState([]); @@ -74,7 +99,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } const tableMetaData = new QTableMetaData(data.childTableMetaData); - const rows = DataGridUtils.makeRows(records, tableMetaData); + const rows = DataGridUtils.makeRows(records, tableMetaData, true); ///////////////////////////////////////////////////////////////////////////////// // note - tablePath may be null, if the user doesn't have access to the table. // @@ -103,6 +128,28 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } } + //////////////////////////////////// + // add actions cell, if available // + //////////////////////////////////// + if(allowRecordEdit || allowRecordDelete) + { + columns.unshift({ + field: "_actions", + type: "string", + headerName: "Actions", + sortable: false, + filterable: false, + width: allowRecordEdit && allowRecordDelete ? 80 : 50, + renderCell: ((params: GridRenderCellParams) => + { + return + {allowRecordEdit && editRecordCallback(params.row.__rowIndex)}>edit} + {allowRecordDelete && deleteRecordCallback(params.row.__rowIndex)}>delete} + + }) + }) + } + setRows(rows); setRecords(records) setColumns(columns); @@ -195,7 +242,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element { disabledFields = data.defaultValuesForNewChildRecords; } - labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields)) + labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback)) } @@ -204,13 +251,18 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element ///////////////////////////////////////////////////////////////// const handleRowClick = (params: GridRowParams, event: MuiEvent, details: GridCallbackDetails) => { + if(disableRowClick) + { + return; + } + (async () => { const qInstance = await qController.loadMetaData() let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name) if(tablePath) { - tablePath = `${tablePath}/${params.id}`; + tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`; DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance); } })(); @@ -266,6 +318,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element rowBuffer={10} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} onRowClick={handleRowClick} + getRowId={(row) => row.__rowIndex} // getRowHeight={() => "auto"} // maybe nice? wraps values in cells... components={{ Toolbar: CustomToolbar diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 0d0a9bd..4cc692f 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -29,11 +29,11 @@ import Tooltip from "@mui/material/Tooltip/Tooltip"; import {GridColDef, GridFilterItem, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams"; -import React from "react"; -import {Link, NavigateFunction} from "react-router-dom"; import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent"; import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React from "react"; +import {Link, NavigateFunction} from "react-router-dom"; const emptyApplyFilterFn = (filterItem: GridFilterItem, column: GridColDef): null => null; @@ -118,7 +118,7 @@ export default class DataGridUtils /******************************************************************************* ** *******************************************************************************/ - public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData): GridRowsProp[] => + public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, allowEmptyId = false): GridRowsProp[] => { const fields = [...tableMetaData.fields.values()]; const rows = [] as any[]; @@ -159,7 +159,10 @@ export default class DataGridUtils ///////////////////////////////////////////////////////////////////////////////////////// // DataGrid gets very upset about a null or undefined here, so, try to make it happier // ///////////////////////////////////////////////////////////////////////////////////////// - row["id"] = "--"; + if(!allowEmptyId) + { + row["id"] = "--"; + } } } @@ -279,7 +282,7 @@ export default class DataGridUtils if (key === tableMetaData.primaryKeyField && linkBase) { column.renderCell = (cellValues: any) => ( - e.stopPropagation()}>{cellValues.value} + cellValues.value ? e.stopPropagation()}>{cellValues.value} : "" ); } }); diff --git a/src/qqq/utils/qqq/TableUtils.ts b/src/qqq/utils/qqq/TableUtils.ts index b1b08f3..bbd199d 100644 --- a/src/qqq/utils/qqq/TableUtils.ts +++ b/src/qqq/utils/qqq/TableUtils.ts @@ -30,61 +30,101 @@ import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin" *******************************************************************************/ class TableUtils { + /******************************************************************************* + ** For a table, return a sub-set of sections (originally meant for display in + ** the record-screen sidebars) ** + ** If the table has no sections, one big "all fields" section is created. + ** + ** a list of "allowed field names" may be given, in which case, a section is only + ** included if it has a field in that list. e.g., an edit-screen, where disabled + ** fields aren't to be shown - if a section only has disabled fields, don't include it. + ** + ** By default sections w/ widget names are excluded -- but -- to include them, + ** provide the metaData plus list of allowedWidgetTypes. *******************************************************************************/ - public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData, allowedKeys: any = null): QTableSection[] + public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData, allowedFieldNames: any = null, additionalInclusionPredicate?: (section: QTableSection) => boolean): QTableSection[] { + ///////////////////////////////////////////////////////////////// + // if the table has sections, then filter them and return some // + ///////////////////////////////////////////////////////////////// if (tableMetaData.sections) { - if (allowedKeys) + ////////////////////////////////////////////////////////////////////////////////////////////// + // if there are filters (a list of allowed field names, or an additionalInclusionPredicate, // + // then only return a subset of sections matching the filters // + ////////////////////////////////////////////////////////////////////////////////////////////// + if (allowedFieldNames || additionalInclusionPredicate) { - const allowedKeySet = new Set(); - allowedKeys.forEach((k: string) => allowedKeySet.add(k)); + //////////////////////////////////////////////////////////////// + // put the field names in a set, for better inclusion testing // + //////////////////////////////////////////////////////////////// + const allowedFieldNameSet = new Set(); + if(allowedFieldNames) + { + allowedFieldNames.forEach((k: string) => allowedFieldNameSet.add(k)); + } + /////////////////////////////////////////////////////////////////////////////// + // loop over the sections, deciding which ones to include in the return list // + /////////////////////////////////////////////////////////////////////////////// const allowedSections: QTableSection[] = []; - for (let i = 0; i < tableMetaData.sections.length; i++) { const section = tableMetaData.sections[i]; - if (section.fieldNames) + let includeSection = false; + + for (let j = 0; j < section.fieldNames?.length; j++) { - for (let j = 0; j < section.fieldNames.length; j++) + if (allowedFieldNameSet.has(section.fieldNames[j])) { - if (allowedKeySet.has(section.fieldNames[j])) - { - allowedSections.push(section); - break; - } + includeSection = true; + break; } } + + if (additionalInclusionPredicate && additionalInclusionPredicate(section)) + { + includeSection = true; + } + + if(includeSection) + { + allowedSections.push(section); + } } + console.log("allowedSections length: " + allowedSections.length); return (allowedSections); } - else - { - return (tableMetaData.sections); - } + + //////////////////////////////////////////////////////////////// + // if there are no filters to apply, then return all sections // + //////////////////////////////////////////////////////////////// + return (tableMetaData.sections); } - else + + /////////////////////////////////////////////////////////////////////////////////////////////// + // else, if the table had no sections, then make a pseudo-one with either all of the fields, // + // or a subset based on the allowedFieldNames // + /////////////////////////////////////////////////////////////////////////////////////////////// + let fieldNames = [...tableMetaData.fields.keys()]; + if (allowedFieldNames) { - let fieldNames = [...tableMetaData.fields.keys()]; - if (allowedKeys) + fieldNames = []; + for (const fieldName in tableMetaData.fields.keys()) { - fieldNames = []; - for (const fieldName in tableMetaData.fields.keys()) + if (allowedFieldNames[fieldName]) { - if (allowedKeys[fieldName]) - { - fieldNames.push(fieldName); - } + fieldNames.push(fieldName); } } - return ([new QTableSection({ - iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...fieldNames], - })]); } + + return ([new QTableSection({ + iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...fieldNames], + })]); } From b8c36bccd28c89f3c5262cc90dda88879ef33ff0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Mar 2024 12:48:16 -0500 Subject: [PATCH 2/6] 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)}> +
+ +
+
+ } + From 5c442b20244287b316abd7a42f037c2293da7a4f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Mar 2024 15:06:02 -0500 Subject: [PATCH 3/6] qqq-frontend-core 1.0.89 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6a2636..71686a6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "file:.yalc/@kingsrook/qqq-frontend-core", + "@kingsrook/qqq-frontend-core": "1.0.89", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", From 6c524c4eca3a115efc4ec01ed9a434f563484677 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Mar 2024 10:41:03 -0500 Subject: [PATCH 4/6] CE-936 - Fix editing child records; fix warning icon on view screen --- src/qqq/components/forms/EntityForm.tsx | 6 +++--- src/qqq/components/widgets/misc/RecordGridWidget.tsx | 2 +- src/qqq/pages/records/view/RecordView.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 6a2134c..f990fc9 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -243,10 +243,10 @@ function EntityForm(props: Props): JSX.Element switch(action) { case "insert": - newChildListWidgetData[widgetName].queryOutput.records.push(new QRecord({values: values})) + newChildListWidgetData[widgetName].queryOutput.records.push({values: values}) break; case "edit": - newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = new QRecord({values: values}); + newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values}; break; case "delete": newChildListWidgetData[widgetName].queryOutput.records.splice(rowIndex, 1); @@ -747,7 +747,7 @@ function EntityForm(props: Props): JSX.Element } associationsToPost[manageAssociationName] = []; haveAssociationsToPost = true; - for(let i=0; i + warning} onClose={() => { setWarningMessage(null); }}> From 84e627467fa02d4e0dffe4d551063afe24071de9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Mar 2024 11:13:58 -0500 Subject: [PATCH 5/6] CE-936 - Update to receive warnings within the response QRecord and display them (this fixes inserts that warn) --- package.json | 2 +- src/qqq/components/forms/EntityForm.tsx | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 71686a6..71f4964 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.89", + "@kingsrook/qqq-frontend-core": "1.0.90", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index f990fc9..74f24f0 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -759,7 +759,9 @@ function EntityForm(props: Props): JSX.Element if (props.id !== null && !props.isCopy) { - // todo - audit that it's a dupe + /////////////////////// + // perform an update // + /////////////////////// await qController .update(tableName, props.id, valuesToPost) .then((record) => @@ -770,8 +772,14 @@ function EntityForm(props: Props): JSX.Element } 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}}); + navigate(path, {state: {updateSuccess: true, warning: warningMessage}}); } }) .catch((error) => @@ -793,6 +801,10 @@ function EntityForm(props: Props): JSX.Element } else { + ///////////////////////////////// + // perform an insert // + // todo - audit if it's a dupe // + ///////////////////////////////// await qController .create(tableName, valuesToPost) .then((record) => @@ -803,10 +815,16 @@ function EntityForm(props: Props): JSX.Element } 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}}); + navigate(path, {state: {createSuccess: true, warning: warningMessage}}); } }) .catch((error) => From c08696b3a1f13028a90a7b7733c48f84152c816c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 20 Mar 2024 10:34:37 -0500 Subject: [PATCH 6/6] Remove todo no longer needed --- src/qqq/components/forms/EntityForm.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 74f24f0..2d6cbc0 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -734,8 +734,6 @@ 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))