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], + })]); }