mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 21:30:45 +00:00
CE-1068 - Add dynamic form widget; add widgets on processes
This commit is contained in:
@ -38,6 +38,7 @@ import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
||||
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 PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
||||
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
||||
@ -261,7 +262,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
{
|
||||
const rs: {[name: string]: any} = {};
|
||||
|
||||
if(record.values)
|
||||
if(record && record.values)
|
||||
{
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -21,8 +21,7 @@
|
||||
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {InputLabel} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {Box, InputLabel} from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Icon from "@mui/material/Icon";
|
||||
|
264
src/qqq/components/widgets/misc/DynamicFormWidget.tsx
Normal file
264
src/qqq/components/widgets/misc/DynamicFormWidget.tsx
Normal 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>);
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
if(data && data.viewAllLink)
|
||||
{
|
||||
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>
|
||||
</Typography>
|
||||
)
|
||||
@ -225,8 +225,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
if(widgetMetaData?.showExportButton)
|
||||
{
|
||||
labelAdditionalElementsLeft.push(
|
||||
<Typography key={1} 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>
|
||||
<Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -305,48 +305,50 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||
>
|
||||
<Box mx={-2} mb={-3}>
|
||||
<DataGridPro
|
||||
autoHeight
|
||||
sx={{
|
||||
borderBottom: "none",
|
||||
borderLeft: "none",
|
||||
borderRight: "none"
|
||||
}}
|
||||
rows={rows}
|
||||
disableSelectionOnClick
|
||||
columns={columns}
|
||||
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
|
||||
}}
|
||||
// pinnedColumns={pinnedColumns}
|
||||
// onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||
// pagination
|
||||
// paginationMode="server"
|
||||
// rowsPerPageOptions={[20]}
|
||||
// sortingMode="server"
|
||||
// filterMode="server"
|
||||
// page={pageNumber}
|
||||
// checkboxSelection
|
||||
rowCount={data && data.totalRows}
|
||||
// onPageSizeChange={handleRowsPerPageChange}
|
||||
// onStateChange={handleStateChange}
|
||||
// density={density}
|
||||
// loading={loading}
|
||||
// filterModel={filterModel}
|
||||
// onFilterModelChange={handleFilterChange}
|
||||
// columnVisibilityModel={columnVisibilityModel}
|
||||
// onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
// onColumnOrderChange={handleColumnOrderChange}
|
||||
// onSelectionModelChange={selectionChanged}
|
||||
// onSortModelChange={handleSortChange}
|
||||
// sortingOrder={[ "asc", "desc" ]}
|
||||
// sortModel={columnSortModel}
|
||||
/>
|
||||
<Box className="recordGridWidget">
|
||||
<DataGridPro
|
||||
autoHeight
|
||||
sx={{
|
||||
borderBottom: "none",
|
||||
borderLeft: "none",
|
||||
borderRight: "none"
|
||||
}}
|
||||
rows={rows}
|
||||
disableSelectionOnClick
|
||||
columns={columns}
|
||||
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
|
||||
}}
|
||||
// pinnedColumns={pinnedColumns}
|
||||
// onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||
// pagination
|
||||
// paginationMode="server"
|
||||
// rowsPerPageOptions={[20]}
|
||||
// sortingMode="server"
|
||||
// filterMode="server"
|
||||
// page={pageNumber}
|
||||
// checkboxSelection
|
||||
rowCount={data && data.totalRows}
|
||||
// onPageSizeChange={handleRowsPerPageChange}
|
||||
// onStateChange={handleStateChange}
|
||||
// density={density}
|
||||
// loading={loading}
|
||||
// filterModel={filterModel}
|
||||
// onFilterModelChange={handleFilterChange}
|
||||
// columnVisibilityModel={columnVisibilityModel}
|
||||
// onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
// onColumnOrderChange={handleColumnOrderChange}
|
||||
// onSelectionModelChange={selectionChanged}
|
||||
// onSortModelChange={handleSortChange}
|
||||
// sortingOrder={[ "asc", "desc" ]}
|
||||
// sortModel={columnSortModel}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Widget>
|
||||
);
|
||||
|
Reference in New Issue
Block a user