CE-1068 - Add dynamic form widget; add widgets on processes

This commit is contained in:
2024-04-30 10:17:38 -05:00
parent e7d870a7fa
commit 8dc8ae0b6d
10 changed files with 495 additions and 87 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.96", "@kingsrook/qqq-frontend-core": "1.0.99",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -175,7 +175,7 @@ class DynamicFormUtils
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
}; };
} }
else else if(processName)
{ {
dynamicFormFields[field.name].possibleValueProps = dynamicFormFields[field.name].possibleValueProps =
{ {
@ -184,6 +184,14 @@ class DynamicFormUtils
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
}; };
} }
else
{
dynamicFormFields[field.name].possibleValueProps =
{
isPossibleValue: true,
initialDisplayValue: initialDisplayValue,
};
}
} }
} }
} }

View File

@ -43,6 +43,7 @@ import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent"; import HelpContent from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget"; import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget"; import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget";
@ -409,6 +410,19 @@ function EntityForm(props: Props): JSX.Element
/> />
} }
if(widgetMetaData.type == "dynamicForm")
{
return <DynamicFormWidget
key={formValues["savedReportId"]} // todo - pull this from the metaData (could do so above too...)
isEditable={true}
widgetMetaData={widgetMetaData}
widgetData={widgetData}
recordValues={formValues}
record={record}
onSaveCallback={setFormFieldValuesFromWidget}
/>
}
return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>) return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>)
} }
@ -482,7 +496,7 @@ function EntityForm(props: Props): JSX.Element
return (true); return (true);
} }
if(widget.type == "reportSetup" || widget.type == "pivotTableSetup") if(widget.type == "reportSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
{ {
return (true); return (true);
} }
@ -706,7 +720,8 @@ function EntityForm(props: Props): JSX.Element
else else
{ {
const widgetMetaData = metaData.widgets.get(section.widgetName); const widgetMetaData = metaData.widgets.get(section.widgetName);
const widgetData = await qController.widget(widgetMetaData.name, props.id ? `${tableMetaData.primaryKeyField}=${props.id}` : ""); const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData); newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
newChildListWidgetData[section.widgetName] = widgetData; newChildListWidgetData[section.widgetName] = widgetData;
} }
@ -966,6 +981,51 @@ function EntityForm(props: Props): JSX.Element
}; };
/*******************************************************************************
**
*******************************************************************************/
function makeQueryStringWithIdAndObject(tableMetaData: QTableMetaData, object: {[key: string]: any})
{
const queryParamsArray: string[] = [];
if(props.id)
{
queryParamsArray.push(`${tableMetaData.primaryKeyField}=${encodeURIComponent(props.id)}`)
}
if(object)
{
for (let key in object)
{
queryParamsArray.push(`${key}=${encodeURIComponent(object[key])}`)
}
}
return (queryParamsArray.join("&"));
}
/*******************************************************************************
**
*******************************************************************************/
async function reloadWidget(widgetName: string, additionalQueryParamsForWidget: {[key: string]: any })
{
const widgetData = await qController.widget(widgetName, makeQueryStringWithIdAndObject(tableMetaData, additionalQueryParamsForWidget));
const widgetMetaData = metaData.widgets.get(widgetName);
/////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - rename this - it holds all widget dta, not just child-lists. also, the type is wrong... //
/////////////////////////////////////////////////////////////////////////////////////////////////////
const newChildListWidgetData: { [name: string]: ChildRecordListData } = Object.assign({}, childListWidgetData);
newChildListWidgetData[widgetName] = widgetData;
setChildListWidgetData(newChildListWidgetData);
const newRenderedWidgetSections = Object.assign({}, renderedWidgetSections);
newRenderedWidgetSections[widgetName] = getWidgetSection(widgetMetaData, widgetData);
setRenderedWidgetSections(newRenderedWidgetSections);
forceUpdate();
}
/******************************************************************************* /*******************************************************************************
** process a form-field having a changed value (e.g., apply field rules). ** process a form-field having a changed value (e.g., apply field rules).
*******************************************************************************/ *******************************************************************************/
@ -981,6 +1041,10 @@ function EntityForm(props: Props): JSX.Element
console.log(`Clearing value from [${fieldRule.targetField}] due to change in [${fieldName}]`); console.log(`Clearing value from [${fieldRule.targetField}] due to change in [${fieldName}]`);
valueChangesToMake[fieldRule.targetField] = null; valueChangesToMake[fieldRule.targetField] = null;
break; break;
case FieldRuleAction.RELOAD_WIDGET:
const additionalQueryParamsForWidget: {[key: string]: any} = {};
additionalQueryParamsForWidget[fieldRule.sourceField] = newValue;
reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget);
} }
} }
} }

View File

@ -38,6 +38,7 @@ import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
import CompositeWidget from "qqq/components/widgets/CompositeWidget"; import CompositeWidget from "qqq/components/widgets/CompositeWidget";
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer"; import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
import DividerWidget from "qqq/components/widgets/misc/Divider"; import DividerWidget from "qqq/components/widgets/misc/Divider";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget"; import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget"; import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart"; import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
@ -261,7 +262,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{ {
const rs: {[name: string]: any} = {}; const rs: {[name: string]: any} = {};
if(record.values) if(record && record.values)
{ {
record.values.forEach((value, key) => rs[key] = value); record.values.forEach((value, key) => rs[key] = value);
} }
@ -596,6 +597,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{}} /> {}} />
) )
} }
{
widgetMetaData.type === "dynamicForm" && (
widgetData && widgetData[i] &&
<DynamicFormWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} recordValues={convertQRecordValuesFromMapToObject(record)} />
)
}
</Box> </Box>
); );
}; };

View File

@ -21,8 +21,7 @@
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {InputLabel} from "@mui/material"; import {Box, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";

View File

@ -0,0 +1,264 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box";
import {FormikContextType, useFormikContext} from "formik";
import QDynamicForm from "qqq/components/forms/DynamicForm";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import Widget from "qqq/components/widgets/Widget";
import {renderSectionOfFields} from "qqq/pages/records/view/RecordView";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useState} from "react";
/*******************************************************************************
** component props
*******************************************************************************/
interface DynamicFormWidgetProps
{
isEditable: boolean;
widgetMetaData: QWidgetMetaData;
widgetData: any;
record: QRecord;
recordValues: { [name: string]: any };
onSaveCallback?: (values: { [name: string]: any }) => void;
}
/*******************************************************************************
** default values for props
*******************************************************************************/
DynamicFormWidget.defaultProps = {
onSaveCallback: null
};
/*******************************************************************************
** Component to display a dynamic form - e.g., on a record edit or view screen,
** or even within a process.
*******************************************************************************/
export default function DynamicFormWidget({isEditable, widgetMetaData, widgetData, record, recordValues, onSaveCallback}: DynamicFormWidgetProps): JSX.Element
{
const [fields, setFields] = useState([] as QFieldMetaData[]);
const [effectiveIsEditable, setEffectiveIsEditable] = useState(isEditable);
if(widgetMetaData.defaultValues.has("isEditable"))
{
const defaultIsEditableValue = widgetMetaData.defaultValues.get("isEditable")
if(defaultIsEditableValue != effectiveIsEditable)
{
setEffectiveIsEditable(defaultIsEditableValue);
}
}
const [dynamicFormFields, setDynamicFormFields] = useState(null as any);
const [formValidations, setFormValidations] = useState(null as any);
const [lastKnowFormValues, setLastKnowFormValues] = useState({} as {[name: string]: any});
//////////////////////////////////////////////////////////////////////////////////////////
// on initial load, and any time widgetData changes (e.g., if widget gets re-rendered), //
// figure out what our form fields are //
//////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
setDynamicFormFields({})
setFormValidations({})
if(widgetData && widgetData.fieldList)
{
const newFields: QFieldMetaData[] = [];
for (let i = 0; i < widgetData.fieldList.length; i++)
{
newFields.push(new QFieldMetaData(widgetData.fieldList[i]));
}
setFields(newFields);
if(newFields.length > 0)
{
const {dynamicFormFields: newDynamicFormFields, formValidations: newFormValidations} = DynamicFormUtils.getFormData(newFields);
const defaultDisplayValues = new Map<string,string>(); // todo - seems not right?
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, newFields, recordValues.tableName, null, record ? record.displayValues : defaultDisplayValues);
setDynamicFormFields(newDynamicFormFields)
setFormValidations(newFormValidations)
}
setLastKnowFormValues({});
}
else
{
setFields([])
}
}, [widgetData]);
/*******************************************************************************
**
*******************************************************************************/
function checkForFormValueChanges(formikProps: FormikContextType<any>)
{
if(!fields || !fields.length)
{
return;
}
let anyChanged = false;
for (let i = 0; i < fields.length; i++)
{
const name = fields[i].name;
if(formikProps.values[name] != lastKnowFormValues[name])
{
anyChanged = true;
lastKnowFormValues[name] = formikProps.values[name];
}
}
if(anyChanged)
{
const mergedDynamicFormValuesIntoFieldName = widgetData.mergedDynamicFormValuesIntoFieldName;
if(mergedDynamicFormValuesIntoFieldName && onSaveCallback)
{
const onSaveCallbackParam: {[name: string]: any} = {};
onSaveCallbackParam[mergedDynamicFormValuesIntoFieldName] = JSON.stringify(lastKnowFormValues);
onSaveCallback(onSaveCallbackParam);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
function getInitialValue(fieldName: string)
{
for (let i = 0; i < fields?.length; i++)
{
if(fields[i].name == fieldName && fields[i].defaultValue)
{
return (fields[i].defaultValue)
}
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
function renderEditForm()
{
const formikProps = useFormikContext();
if(!fields || !fields.length)
{
return (
<Box>
<Box fontSize="1rem">{widgetData && widgetData.noFieldsMessage}</Box>
</Box>
);
}
const formData: any = {};
formData.values = formikProps.values;
formData.touched = formikProps.touched;
formData.errors = formikProps.errors;
formData.formFields = {};
// todo - merge the formValidations object with formik's - maybe in the useEffect where we build it
// setValidations(Yup.object().shape(formValidations));
// formikProps.validationSchema.
for (let key of Object.keys(dynamicFormFields))
{
const dynamicFormField = dynamicFormFields[key];
formData.formFields[dynamicFormField.name] = dynamicFormField;
const initialValue = getInitialValue(dynamicFormField.name);
if(initialValue != null)
{
console.log(`@dk trying to set an initial value [${dynamicFormField.name}] to [${initialValue}]`);
// @ts-ignore some any
formikProps.initialValues[dynamicFormField.name] = initialValue;
}
}
if(formData.values)
{
checkForFormValueChanges(formikProps);
}
return (
<Box>
<QDynamicForm formData={formData} record={record} />
</Box>
);
}
/*******************************************************************************
**
*******************************************************************************/
function renderViewForm()
{
const fieldNames: string[] = [];
const fieldMap: {[name: string]: QFieldMetaData} = {};
const fakeRecord = new QRecord({});
const mergedDynamicFormValuesIntoFieldName = widgetData.mergedDynamicFormValuesIntoFieldName;
for (let i = 0; i < fields?.length; i++)
{
const fieldName = fields[i].name;
fieldNames.push(fieldName);
fieldMap[fieldName] = fields[i];
if(mergedDynamicFormValuesIntoFieldName && recordValues[mergedDynamicFormValuesIntoFieldName])
{
fakeRecord.values.set(fieldName, recordValues[mergedDynamicFormValuesIntoFieldName][fieldName]);
}
}
const section = renderSectionOfFields(`dynamicFormWidget:${widgetMetaData.name}`, fieldNames, null, false, fakeRecord, fieldMap);
return (<Box>
{section}
</Box>);
}
////////////
// render //
////////////
return (<Widget widgetMetaData={widgetMetaData}>
{
<React.Fragment>
{effectiveIsEditable ? renderEditForm() : renderViewForm()}
</React.Fragment>
}
</Widget>);
}

View File

@ -185,7 +185,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
if(data && data.viewAllLink) if(data && data.viewAllLink)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative"> <Typography key={"viewAllLink"} variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
<Link to={data.viewAllLink}>View All</Link> <Link to={data.viewAllLink}>View All</Link>
</Typography> </Typography>
) )
@ -225,8 +225,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
if(widgetMetaData?.showExportButton) if(widgetMetaData?.showExportButton)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography key={1} variant="body2" px={0} display="inline" position="relative"> <Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative">
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip> <Tooltip title={tooltipTitle}><span><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></span></Tooltip>
</Typography> </Typography>
); );
} }
@ -305,48 +305,50 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
> >
<Box mx={-2} mb={-3}> <Box mx={-2} mb={-3}>
<DataGridPro <Box className="recordGridWidget">
autoHeight <DataGridPro
sx={{ autoHeight
borderBottom: "none", sx={{
borderLeft: "none", borderBottom: "none",
borderRight: "none" borderLeft: "none",
}} borderRight: "none"
rows={rows} }}
disableSelectionOnClick rows={rows}
columns={columns} disableSelectionOnClick
rowBuffer={10} columns={columns}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} rowBuffer={10}
onRowClick={handleRowClick} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
getRowId={(row) => row.__rowIndex} onRowClick={handleRowClick}
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells... getRowId={(row) => row.__rowIndex}
components={{ // getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
Toolbar: CustomToolbar components={{
}} Toolbar: CustomToolbar
// pinnedColumns={pinnedColumns} }}
// onPinnedColumnsChange={handlePinnedColumnsChange} // pinnedColumns={pinnedColumns}
// pagination // onPinnedColumnsChange={handlePinnedColumnsChange}
// paginationMode="server" // pagination
// rowsPerPageOptions={[20]} // paginationMode="server"
// sortingMode="server" // rowsPerPageOptions={[20]}
// filterMode="server" // sortingMode="server"
// page={pageNumber} // filterMode="server"
// checkboxSelection // page={pageNumber}
rowCount={data && data.totalRows} // checkboxSelection
// onPageSizeChange={handleRowsPerPageChange} rowCount={data && data.totalRows}
// onStateChange={handleStateChange} // onPageSizeChange={handleRowsPerPageChange}
// density={density} // onStateChange={handleStateChange}
// loading={loading} // density={density}
// filterModel={filterModel} // loading={loading}
// onFilterModelChange={handleFilterChange} // filterModel={filterModel}
// columnVisibilityModel={columnVisibilityModel} // onFilterModelChange={handleFilterChange}
// onColumnVisibilityModelChange={handleColumnVisibilityChange} // columnVisibilityModel={columnVisibilityModel}
// onColumnOrderChange={handleColumnOrderChange} // onColumnVisibilityModelChange={handleColumnVisibilityChange}
// onSelectionModelChange={selectionChanged} // onColumnOrderChange={handleColumnOrderChange}
// onSortModelChange={handleSortChange} // onSelectionModelChange={selectionChanged}
// sortingOrder={[ "asc", "desc" ]} // onSortModelChange={handleSortChange}
// sortModel={columnSortModel} // sortingOrder={[ "asc", "desc" ]}
/> // sortModel={columnSortModel}
/>
</Box>
</Box> </Box>
</Widget> </Widget>
); );

View File

@ -58,6 +58,7 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
import ValidationReview from "qqq/components/processes/ValidationReview"; import ValidationReview from "qqq/components/processes/ValidationReview";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery"; import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
@ -124,6 +125,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [showErrorDetail, setShowErrorDetail] = useState(false); const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = useState(false); const [showFullHelpText, setShowFullHelpText] = useState(false);
const [renderedWidgets, setRenderedWidgets] = useState({} as {[step: string]: {[widgetName: string]: any}});
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext); const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -273,6 +276,42 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}; };
}; };
/*******************************************************************************
**
*******************************************************************************/
function renderWidget(widgetName: string)
{
if(!renderedWidgets[activeStep.name])
{
renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets);
}
if(renderedWidgets[activeStep.name][widgetName])
{
return renderedWidgets[activeStep.name][widgetName];
}
const widgetMetaData = qInstance.widgets.get(widgetName);
if(!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
const queryStringParts: string[] = [];
for (let name in processValues)
{
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`)
}
const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
</Box>)
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget;
}
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// generate the main form body content for a step // // generate the main form body content for a step //
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
@ -653,6 +692,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</Box> </Box>
) )
} }
{
component.type === QComponentType.WIDGET && (
component.values?.widgetName &&
renderWidget(component.values?.widgetName)
)
}
</div> </div>
); );
})) }))

View File

@ -21,6 +21,7 @@
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability"; import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
@ -82,6 +83,47 @@ RecordView.defaultProps =
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant"; const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
/*******************************************************************************
**
*******************************************************************************/
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: {[name: string]: QFieldMetaData} )
{
return <Box key={key} display="flex" flexDirection="column" py={1} pr={2}>
{
fieldNames.map((fieldName: string) =>
{
let [field, tableForField] = tableMetaData ? TableUtils.getFieldAndTable(tableMetaData, fieldName) : fieldMap ? [fieldMap[fieldName], null] : [null, null];
if (field != null)
{
let label = field.label;
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={label} helpContentKey={`table:${tableMetaData?.name};field:${fieldName}`} />;
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default"}}>{label}:</Typography>;
return (
<Box key={fieldName} flexDirection="row" pr={2}>
<>
{
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
}
<div style={{display: "inline-block", width: 0}}>&nbsp;</div>
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
</Typography>
</>
</Box>
);
}
})
}
</Box>;
}
function RecordView({table, launchProcess}: Props): JSX.Element function RecordView({table, launchProcess}: Props): JSX.Element
{ {
const {id} = useParams(); const {id} = useParams();
@ -519,40 +561,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
// for a section with field names, render the field values. // // for a section with field names, render the field values. //
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. // // for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
const fields = ( const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record);
<Box key={section.name} display="flex" flexDirection="column" py={1} pr={2}>
{
section.fieldNames.map((fieldName: string) =>
{
let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
if (field != null)
{
let label = field.label;
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={label} helpContentKey={`table:${tableName};field:${fieldName}`} />;
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default"}}>{label}:</Typography>;
return (
<Box key={fieldName} flexDirection="row" pr={2}>
<>
{
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
}
<div style={{display: "inline-block", width: 0}}>&nbsp;</div>
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
</Typography>
</>
</Box>
);
}
})
}
</Box>
);
if (section.tier === "T1") if (section.tier === "T1")
{ {
@ -979,7 +988,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
showEditChildForm && showEditChildForm &&
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}> <Modal open={showEditChildForm !== null} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm"> <div className="modalEditForm">
<EntityForm <EntityForm
isModal={true} isModal={true}

View File

@ -688,6 +688,16 @@ input[type="search"]::-webkit-search-results-decoration
font-weight: 500; font-weight: 500;
} }
.recordView .widget
{
padding: 24px;
}
.recordView .widget .recordGridWidget
{
margin: -8px;
}
.MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover .MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover
{ {
color: white; color: white;