Add add-child capability to recordGridWidget; making new standard Widget that others can(should) use

This commit is contained in:
2022-12-05 16:08:50 -06:00
parent 83e0084f2f
commit 18c15543f9
11 changed files with 564 additions and 165 deletions

View File

@ -1,10 +1,13 @@
/// <reference types="cypress-wait-for-stable-dom" /> /// <reference types="cypress-wait-for-stable-dom" />
import QLib from "./lib/qLib";
describe("table query screen", () => describe("table query screen", () =>
{ {
before(() => before(() =>
{ {
QLib.init(cy);
cy.intercept("GET", "/metaData/authentication", {fixture: "metaData/authentication.json"}).as("authenticationMetaData"); cy.intercept("GET", "/metaData/authentication", {fixture: "metaData/authentication.json"}).as("authenticationMetaData");
cy.intercept("GET", "/metaData", {fixture: "metaData/index.json"}).as("metaData"); cy.intercept("GET", "/metaData", {fixture: "metaData/index.json"}).as("metaData");
cy.intercept("GET", "/metaData/table/person", {fixture: "metaData/table/person.json"}).as("personMetaData"); cy.intercept("GET", "/metaData/table/person", {fixture: "metaData/table/person.json"}).as("personMetaData");
@ -29,14 +32,13 @@ describe("table query screen", () =>
}); });
}); });
it.only("can add query filters", () => it("can add query filters", () =>
{ {
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
// go to table, wait for filter to run, and rows to appear // // go to table, wait for filter to run, and rows to appear //
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
cy.visit("https://localhost:3000/peopleApp/greetingsApp/person"); cy.visit("https://localhost:3000/peopleApp/greetingsApp/person");
cy.wait(["@personQuery", "@personCount"]); QLib.waitForQueryScreen();
cy.get(".MuiDataGrid-virtualScrollerRenderZone").children().should("have.length.greaterThan", 3);
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
// open the filter window, enter a value, wait for query to re-run // // open the filter window, enter a value, wait for query to re-run //
@ -72,6 +74,29 @@ describe("table query screen", () =>
cy.contains(".MuiDataGrid-toolbarContainer .MuiBadge-root", "1").should("not.exist"); cy.contains(".MuiDataGrid-toolbarContainer .MuiBadge-root", "1").should("not.exist");
}); });
it.only("can do a boolean or query", () =>
{
/////////////////////////////////////////
// go to table, wait for filter to run //
/////////////////////////////////////////
cy.visit("https://localhost:3000/peopleApp/greetingsApp/person");
QLib.waitForQueryScreen();
QLib.buildEntityListQueryFilter([
{fieldLabel: "First Name", operator: "contains", textValue: "Dar"},
{fieldLabel: "First Name", operator: "contains", textValue: "Jam"}
], "or");
let expectedFilterContents0 = JSON.stringify({fieldName: "firstName", operator: "CONTAINS", values: ["Dar"]});
let expectedFilterContents1 = JSON.stringify({fieldName: "firstName", operator: "CONTAINS", values: ["Jam"]});
cy.wait("@personQuery").its("request.body").should((body) =>
{
expect(body).to.contain(expectedFilterContents0);
expect(body).to.contain(expectedFilterContents1);
expect(body).to.contain("asdf");
});
});
// tests to add: // tests to add:
// - filter boolean OR // - filter boolean OR
// - sort column // - sort column

86
cypress/e2e/lib/qLib.ts Normal file
View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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/>.
*/
export default class QLib
{
// @ts-ignore
private static cy: Cypress.cy;
// @ts-ignore
public static init(cy: Cypress.cy)
{
QLib.cy = cy;
}
/*******************************************************************************
** Wait for a query to finish on the entity-list screen. specifically, wait for
** personQuery & personCount requests, and wait for the data grid to have rows.
*******************************************************************************/
public static waitForQueryScreen()
{
QLib.cy.wait(["@personQuery", "@personCount"]);
QLib.cy.get(".MuiDataGrid-virtualScrollerRenderZone").children().should("have.length.greaterThan", 3);
}
/*******************************************************************************
** Open the Filters drop down, and build a query
*******************************************************************************/
public static buildEntityListQueryFilter(input: QueryFilterInput | QueryFilterInput[], booleanOperator: ("and" | "or") = "and")
{
QLib.cy.contains("Filters").click();
if ((input as QueryFilterInput).fieldLabel)
{
const queryFilterInput = input as QueryFilterInput;
QLib.addSingleQueryFilterInput(queryFilterInput, 0, booleanOperator);
}
else
{
const inputArray = input as QueryFilterInput[];
inputArray.forEach((qfi, index) => QLib.addSingleQueryFilterInput(qfi, index, booleanOperator));
}
}
/*******************************************************************************
** private helper - adds 1 query filter input.
*******************************************************************************/
private static addSingleQueryFilterInput(queryFilterInput: QueryFilterInput, index: number, booleanOperator: ("and" | "or"))
{
if (index > 0)
{
QLib.cy.contains("Add filter").click();
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormLinkOperatorInput SELECT").select(booleanOperator);
}
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormColumnInput SELECT").select(queryFilterInput.fieldLabel);
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormOperatorInput SELECT").select(queryFilterInput.operator);
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormValueInput INPUT").type(queryFilterInput.textValue);
}
}
interface QueryFilterInput
{
fieldLabel?: string;
fieldName?: string;
operator?: string;
textValue?: string;
}

View File

@ -21,6 +21,7 @@
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Skeleton} from "@mui/material"; import {Skeleton} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import parse from "html-react-parser"; import parse from "html-react-parser";
@ -37,6 +38,7 @@ import QuickSightChart from "qqq/pages/dashboards/Widgets/QuickSightChart";
import RecordGridWidget from "qqq/pages/dashboards/Widgets/RecordGridWidget"; import RecordGridWidget from "qqq/pages/dashboards/Widgets/RecordGridWidget";
import StepperCard from "qqq/pages/dashboards/Widgets/StepperCard"; import StepperCard from "qqq/pages/dashboards/Widgets/StepperCard";
import TableCard from "qqq/pages/dashboards/Widgets/TableCard"; import TableCard from "qqq/pages/dashboards/Widgets/TableCard";
import Widget from "qqq/pages/dashboards/Widgets/Widget";
import ProcessRun from "qqq/pages/process-run"; import ProcessRun from "qqq/pages/process-run";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
@ -116,7 +118,7 @@ function DashboardWidgets({widgetMetaDataList, entityPrimaryKey, omitWrappingGri
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element => const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
{ {
return ( return (
<MDBox sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}> <MDBox key={i} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
{ {
widgetMetaData.type === "parentWidget" && ( widgetMetaData.type === "parentWidget" && (
<ParentWidget <ParentWidget
@ -165,11 +167,8 @@ function DashboardWidgets({widgetMetaDataList, entityPrimaryKey, omitWrappingGri
} }
{ {
widgetMetaData.type === "html" && ( widgetMetaData.type === "html" && (
<Card sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}> <Widget label={widgetMetaData.label}>
<MDBox padding="1rem"> <Box px={1} pt={0} pb={2}>
<MDTypography variant="h5" textTransform="capitalize">
{widgetMetaData.label}
</MDTypography>
<MDTypography component="div" variant="button" color="text" fontWeight="light"> <MDTypography component="div" variant="button" color="text" fontWeight="light">
{ {
widgetData && widgetData[i] && widgetData[i].html ? ( widgetData && widgetData[i] && widgetData[i].html ? (
@ -177,8 +176,8 @@ function DashboardWidgets({widgetMetaDataList, entityPrimaryKey, omitWrappingGri
) : <Skeleton /> ) : <Skeleton />
} }
</MDTypography> </MDTypography>
</MDBox> </Box>
</Card> </Widget>
) )
} }
{ {
@ -233,6 +232,7 @@ function DashboardWidgets({widgetMetaDataList, entityPrimaryKey, omitWrappingGri
<RecordGridWidget <RecordGridWidget
title={widgetMetaData.label} title={widgetMetaData.label}
data={widgetData[i]} data={widgetData[i]}
reloadWidgetCallback={reloadWidget}
/> />
) )
} }

View File

@ -24,9 +24,11 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert} from "@mui/material"; import {Alert} from "@mui/material";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
@ -49,10 +51,23 @@ import QValueUtils from "qqq/utils/QValueUtils";
interface Props interface Props
{ {
id?: string; id?: string;
isModal: boolean;
table?: QTableMetaData; table?: QTableMetaData;
closeModalHandler?: (event: object, reason: string) => void;
defaultValues: { [key: string]: string };
disabledFields: { [key: string]: boolean };
} }
function EntityForm({table, id}: Props): JSX.Element EntityForm.defaultProps = {
id: null,
isModal: false,
table: null,
closeModalHandler: null,
defaultValues: {},
disabledFields: {},
};
function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disabledFields}: Props): JSX.Element
{ {
const qController = QClient.getInstance(); const qController = QClient.getInstance();
const tableNameParam = useParams().tableName; const tableNameParam = useParams().tableName;
@ -126,39 +141,89 @@ function EntityForm({table, id}: Props): JSX.Element
// if doing an edit, fetch the record and pre-populate the form values from it // // if doing an edit, fetch the record and pre-populate the form values from it //
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null; let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (id !== null) if (id !== null)
{ {
record = await qController.get(tableName, id); record = await qController.get(tableName, id);
setRecord(record); setRecord(record);
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`); setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
setPageHeader(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!isModal)
{
setPageHeader(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
}
tableMetaData.fields.forEach((fieldMetaData, key) => tableMetaData.fields.forEach((fieldMetaData, key) =>
{ {
initialValues[key] = record.values.get(key); initialValues[key] = record.values.get(key);
if(fieldMetaData.type == QFieldType.DATE_TIME)
{
initialValues[key] = QValueUtils.formatDateTimeValueForForm(record.values.get(key));
}
}); });
setFormValues(formValues); //? safe to delete? setFormValues(formValues);
if(!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{ {
setNoCapabilityError("You may not edit records in this table"); setNoCapabilityError("You may not edit records in this table");
} }
} }
else else
{ {
///////////////////////////////////////////
// else handle preparing to do an insert //
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`); setFormTitle(`Creating New ${tableMetaData?.label}`);
setPageHeader(`Creating New ${tableMetaData?.label}`);
if(!tableMetaData.capabilities.has(Capability.TABLE_INSERT)) if (!isModal)
{
setPageHeader(`Creating New ${tableMetaData?.label}`);
}
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{ {
setNoCapabilityError("You may not create records in this table"); setNoCapabilityError("You may not create records in this table");
} }
////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. //
////////////////////////////////////////////////////////////////////////////////////////////////
if(defaultValues)
{
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name;
if (defaultValues[fieldName])
{
initialValues[fieldName] = defaultValues[fieldName];
///////////////////////////////////////////////////////////////////////////////////////////
// we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed //
///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{
defaultDisplayValues.set(fieldName, results[0].label);
}
}
}
}
}
} }
/////////////////////////////////////////////////////////////////////
// make sure all initialValues are properly formatted for the form //
/////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
{
initialValues[fieldMetaData.name] = QValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
}
}
setInitialValues(initialValues); setInitialValues(initialValues);
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
@ -168,7 +233,15 @@ function EntityForm({table, id}: Props): JSX.Element
dynamicFormFields, dynamicFormFields,
formValidations, formValidations,
} = DynamicFormUtils.getFormData(fieldArray); } = DynamicFormUtils.getFormData(fieldArray);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, record?.displayValues); DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, record ? record.displayValues : defaultDisplayValues);
if(disabledFields)
{
for (let fieldName in disabledFields)
{
dynamicFormFields[fieldName].isEditable = false;
}
}
///////////////////////////////////// /////////////////////////////////////
// group the formFields by section // // group the formFields by section //
@ -181,12 +254,7 @@ function EntityForm({table, id}: Props): JSX.Element
const section = tableSections[i]; const section = tableSections[i];
const sectionDynamicFormFields: any[] = []; const sectionDynamicFormFields: any[] = [];
if(section.isHidden) if (section.isHidden || !section.fieldNames)
{
continue;
}
if(!section.fieldNames)
{ {
continue; continue;
} }
@ -271,8 +339,15 @@ function EntityForm({table, id}: Props): JSX.Element
.update(tableName, id, values) .update(tableName, id, values)
.then((record) => .then((record) =>
{ {
const path = `${location.pathname.replace(/\/edit$/, "")}?updateSuccess=true`; if (isModal)
navigate(path); {
closeModalHandler(null, "recordUpdated");
}
else
{
const path = `${location.pathname.replace(/\/edit$/, "")}?updateSuccess=true`;
navigate(path);
}
}) })
.catch((error) => .catch((error) =>
{ {
@ -287,8 +362,15 @@ function EntityForm({table, id}: Props): JSX.Element
.create(tableName, values) .create(tableName, values)
.then((record) => .then((record) =>
{ {
const path = `${location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField))}?createSuccess=true`; if (isModal)
navigate(path); {
closeModalHandler(null, "recordCreated");
}
else
{
const path = `${location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField))}?createSuccess=true`;
navigate(path);
}
}) })
.catch((error) => .catch((error) =>
{ {
@ -300,111 +382,128 @@ function EntityForm({table, id}: Props): JSX.Element
const formId = id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; const formId = id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
if(noCapabilityError) let body;
if (noCapabilityError)
{ {
return <MDBox mb={3}> body = (
<Grid container spacing={3}> <MDBox mb={3}>
<Grid item xs={12}> <Grid container spacing={3}>
<MDBox mb={3}> <Grid item xs={12}>
<Alert severity="error">{noCapabilityError}</Alert>
</MDBox>
</Grid>
</Grid>
</MDBox>;
}
return (
<MDBox mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
{alertContent ? (
<MDBox mb={3}> <MDBox mb={3}>
<Alert severity="error">{alertContent}</Alert> <Alert severity="error">{noCapabilityError}</Alert>
</MDBox> </MDBox>
) : ("")} </Grid>
</Grid> </Grid>
</Grid> </MDBox>
<Grid container spacing={3}> );
<Grid item xs={12} lg={3}> }
<QRecordSidebar tableSections={tableSections} /> else
{
const cardElevation = isModal ? 3 : 1;
body = (
<MDBox mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
{alertContent ? (
<MDBox mb={3}>
<Alert severity="error">{alertContent}</Alert>
</MDBox>
) : ("")}
</Grid>
</Grid> </Grid>
<Grid item xs={12} lg={9}> <Grid container spacing={3}>
{
!isModal &&
<Grid item xs={12} lg={3}>
<QRecordSidebar tableSections={tableSections} />
</Grid>
}
<Grid item xs={12} lg={isModal ? 12 : 9}>
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
validationSchema={validations} validationSchema={validations}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ {({
values, values,
errors, errors,
touched, touched,
isSubmitting, isSubmitting,
}) => ( }) => (
<Form id={formId} autoComplete="off"> <Form id={formId} autoComplete="off">
<MDBox pb={3} pt={0}> <MDBox pb={3} pt={0}>
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}}> <Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}>
<MDBox display="flex" p={3} pb={1}> <MDBox display="flex" p={3} pb={1}>
<MDBox mr={1.5}> <MDBox mr={1.5}>
<Avatar sx={{bgcolor: colors.info.main}}> <Avatar sx={{bgcolor: colors.info.main}}>
<Icon> <Icon>
{tableMetaData?.iconName} {tableMetaData?.iconName}
</Icon> </Icon>
</Avatar> </Avatar>
</MDBox>
<MDBox display="flex" alignItems="center">
<MDTypography variant="h5">{formTitle}</MDTypography>
</MDBox>
</MDBox> </MDBox>
<MDBox display="flex" alignItems="center"> {
<MDTypography variant="h5">{formTitle}</MDTypography> t1sectionName && formFields ? (
</MDBox> <MDBox pb={1} px={3}>
</MDBox> <MDBox p={3} width="100%">
{ {getFormSection(values, touched, formFields.get(t1sectionName), errors)}
t1sectionName && formFields ? ( </MDBox>
<MDBox pb={1} px={3}>
<MDBox p={3} width="100%">
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
</MDBox> </MDBox>
</MDBox> ) : null
) : null }
}
</Card>
</MDBox>
{formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
<MDBox key={`edit-card-${section.name}`} pb={3}>
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}}>
<MDTypography variant="h5" p={3} pb={1}>
{section.label}
</MDTypography>
<MDBox pb={1} px={3}>
<MDBox p={3} width="100%">
{
getFormSection(values, touched, formFields.get(section.name), errors)
}
</MDBox>
</MDBox>
</Card> </Card>
</MDBox> </MDBox>
)) : null} {formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
<MDBox key={`edit-card-${section.name}`} pb={3}>
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}} elevation={cardElevation}>
<MDTypography variant="h5" p={3} pb={1}>
{section.label}
</MDTypography>
<MDBox pb={1} px={3}>
<MDBox p={3} width="100%">
{getFormSection(values, touched, formFields.get(section.name), errors)}
</MDBox>
</MDBox>
</Card>
</MDBox>
)) : null}
<MDBox component="div" p={3}> <MDBox component="div" p={3}>
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} /> <QCancelButton onClickHandler={isModal ? closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} /> <QSaveButton disabled={isSubmitting} />
</Grid> </Grid>
</MDBox> </MDBox>
</Form> </Form>
)} )}
</Formik> </Formik>
</Grid>
</Grid> </Grid>
</Grid> </MDBox>
</MDBox> );
); }
if (isModal)
{
return (
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{my: 5, mx: "auto", p: 6, pb: 0, maxWidth: "1024px"}}>
{body}
</Card>
</Box>
);
}
else
{
return (body);
}
} }
EntityForm.defaultProps = {
id: null,
table: null,
};
export default EntityForm; export default EntityForm;

View File

@ -133,6 +133,7 @@ function QDynamicForm(props: Props): JSX.Element
<QDynamicSelect <QDynamicSelect
tableName={field.possibleValueProps.tableName} tableName={field.possibleValueProps.tableName}
fieldName={fieldName} fieldName={fieldName}
isEditable={field.isEditable}
fieldLabel={field.label} fieldLabel={field.label}
initialValue={values[fieldName]} initialValue={values[fieldName]}
initialDisplayValue={field.possibleValueProps.initialDisplayValue} initialDisplayValue={field.possibleValueProps.initialDisplayValue}

View File

@ -259,7 +259,7 @@ function CarrierPerformance(): JSX.Element
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title={qInstance?.widgets.get("TotalShipmentsStatisticsCard").label} title={qInstance?.widgets?.get("TotalShipmentsStatisticsCard").label}
data={totalShipmentsData} data={totalShipmentsData}
increaseIsGood={true} increaseIsGood={true}
dropdown={{ dropdown={{
@ -271,7 +271,7 @@ function CarrierPerformance(): JSX.Element
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title={qInstance?.widgets.get("SuccessfulDeliveriesStatisticsCard").label} title={qInstance?.widgets?.get("SuccessfulDeliveriesStatisticsCard").label}
data={successfulDeliveriesData} data={successfulDeliveriesData}
increaseIsGood={true} increaseIsGood={true}
dropdown={{ dropdown={{
@ -284,7 +284,7 @@ function CarrierPerformance(): JSX.Element
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title={qInstance?.widgets.get("ServiceFailuresStatisticsCard").label} title={qInstance?.widgets?.get("ServiceFailuresStatisticsCard").label}
data={serviceFailuresData} data={serviceFailuresData}
increaseIsGood={false} increaseIsGood={false}
dropdown={{ dropdown={{
@ -300,7 +300,7 @@ function CarrierPerformance(): JSX.Element
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={6} lg={8}> <Grid item xs={12} sm={6} lg={8}>
<DefaultLineChart <DefaultLineChart
title={qInstance?.widgets.get("CarrierVolumeLineChart").label} title={qInstance?.widgets?.get("CarrierVolumeLineChart").label}
data={carrierVolumeData} data={carrierVolumeData}
/> />
</Grid> </Grid>

View File

@ -265,7 +265,7 @@ function Overview(): JSX.Element
<MDBox mb={3}> <MDBox mb={3}>
<BarChart <BarChart
color="info" color="info"
title={qInstance?.widgets.get("TotalShipmentsByDayBarChart").label} title={qInstance?.widgets?.get("TotalShipmentsByDayBarChart").label}
description={shipmentsByDayDescription} description={shipmentsByDayDescription}
date="Updated 3 minutes ago" date="Updated 3 minutes ago"
data={shipmentsByDayData} data={shipmentsByDayData}
@ -275,7 +275,7 @@ function Overview(): JSX.Element
<Grid item xs={12} md={6} lg={4}> <Grid item xs={12} md={6} lg={4}>
<MDBox mb={3}> <MDBox mb={3}>
<PieChartCard <PieChartCard
title={qInstance?.widgets.get("YTDShipmentsByCarrierPieChart").label} title={qInstance?.widgets?.get("YTDShipmentsByCarrierPieChart").label}
description={shipmentsByCarrierDescription} description={shipmentsByCarrierDescription}
data={shipmentsByCarrierData} data={shipmentsByCarrierData}
/> />
@ -285,7 +285,7 @@ function Overview(): JSX.Element
<MDBox mb={3}> <MDBox mb={3}>
<SmallLineChart <SmallLineChart
color="dark" color="dark"
title={qInstance?.widgets.get("TotalShipmentsByMonthLineChart").label} title={qInstance?.widgets?.get("TotalShipmentsByMonthLineChart").label}
description={shipmentsByMonthDescription} description={shipmentsByMonthDescription}
date="" date=""
chart={shipmentsByMonthData} chart={shipmentsByMonthData}

View File

@ -21,32 +21,28 @@
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box"; import {DataGridPro} from "@mui/x-data-grid-pro";
import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography";
import {DataGridPro, GridValidRowModel} from "@mui/x-data-grid-pro";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {Link} from "react-router-dom";
import MDTypography from "qqq/components/Temporary/MDTypography";
import DataGridUtils from "qqq/utils/DataGridUtils"; import DataGridUtils from "qqq/utils/DataGridUtils";
import Widget, {AddNewRecordButton, HeaderLink} from "./Widget";
interface Props interface Props
{ {
title: string title: string;
data: any; data: any;
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
} }
RecordGridWidget.defaultProps = { RecordGridWidget.defaultProps = {};
};
function RecordGridWidget({title, data}: Props): JSX.Element function RecordGridWidget({title, data, reloadWidgetCallback}: Props): JSX.Element
{ {
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([]);
useEffect(() => useEffect(() =>
{ {
if(data && data.childTableMetaData && data.queryOutput) if (data && data.childTableMetaData && data.queryOutput)
{ {
const records: QRecord[] = []; const records: QRecord[] = [];
const queryOutputRecords = data.queryOutput.records; const queryOutputRecords = data.queryOutput.records;
@ -60,28 +56,26 @@ function RecordGridWidget({title, data}: Props): JSX.Element
const tableMetaData = new QTableMetaData(data.childTableMetaData); const tableMetaData = new QTableMetaData(data.childTableMetaData);
const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData); const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData);
const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, data.tablePath);
const childTablePath = data.tablePath + (data.tablePath.endsWith("/") ? "" : "/")
const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, childTablePath);
setRows(rows); setRows(rows);
setColumns(columns); setColumns(columns);
} }
}, [data]) }, [data]);
return ( return (
<Card sx={{width: "100%"}}> <Widget
<Box display="flex" justifyContent="space-between" alignItems="center"> label={title}
<Typography variant="h5" fontWeight="medium" p={3}> labelAdditionalComponentsLeft={[
{title} new HeaderLink("View All", data.viewAllLink)
</Typography> ]}
{ labelAdditionalComponentsRight={[
data.viewAllLink && new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords)
<Typography variant="body2" p={3}> ]}
<Link to={data.viewAllLink}> reloadWidgetCallback={reloadWidgetCallback}
View All >
</Link>
</Typography>
}
</Box>
<DataGridPro <DataGridPro
autoHeight autoHeight
rows={rows} rows={rows}
@ -115,8 +109,8 @@ function RecordGridWidget({title, data}: Props): JSX.Element
// sortingOrder={[ "asc", "desc" ]} // sortingOrder={[ "asc", "desc" ]}
// sortModel={columnSortModel} // sortModel={columnSortModel}
/> />
</Card> </Widget>
) );
} }
export default RecordGridWidget; export default RecordGridWidget;

View File

@ -0,0 +1,194 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Modal from "@mui/material/Modal";
import Typography from "@mui/material/Typography";
import React, {useState} from "react";
import {Link} from "react-router-dom";
import EntityForm from "qqq/components/EntityForm";
interface Props
{
label: string;
labelAdditionalComponentsLeft: [LabelComponent];
labelAdditionalComponentsRight: [LabelComponent];
children: JSX.Element;
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
}
Widget.defaultProps = {
label: null,
labelAdditionalComponentsLeft: [],
labelAdditionalComponentsRight: [],
};
class LabelComponent
{
}
export class HeaderLink extends LabelComponent
{
label: string;
to: string
constructor(label: string, to: string)
{
super();
this.label = label;
this.to = to;
}
}
export class AddNewRecordButton extends LabelComponent
{
table: QTableMetaData;
label:string;
defaultValues: any;
disabledFields: any;
constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues)
{
super();
this.table = table;
this.label = label;
this.defaultValues = defaultValues;
this.disabledFields = disabledFields;
}
}
function Widget(props: React.PropsWithChildren<Props>): JSX.Element
{
const [showEditForm, setShowEditForm] = useState(null as any);
function openEditForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
{
const showEditForm: any = {};
showEditForm.table = table;
showEditForm.id = id;
showEditForm.defaultValues = defaultValues;
showEditForm.disabledFields = disabledFields;
setShowEditForm(showEditForm);
}
const closeEditForm = (event: object, reason: string) =>
{
if (reason === "backdropClick")
{
return;
}
if (reason === "recordUpdated" || reason === "recordCreated")
{
if(props.reloadWidgetCallback)
{
props.reloadWidgetCallback(0, "ok");
}
else
{
window.location.reload()
}
}
setShowEditForm(null);
};
function renderComponent(component: LabelComponent)
{
if(component instanceof HeaderLink)
{
const link = component as HeaderLink
return (
<Typography variant="body2" p={2} display="inline">
{link.to ? <Link to={link.to}>{link.label}</Link> : null}
</Typography>
);
}
if (component instanceof AddNewRecordButton)
{
const addNewRecordButton = component as AddNewRecordButton
return (
<Typography variant="body2" p={2} pr={1} display="inline">
<Button onClick={() => openEditForm(addNewRecordButton.table, null, addNewRecordButton.defaultValues, addNewRecordButton.disabledFields)}>{addNewRecordButton.label}</Button>
</Typography>
);
}
}
return (
<>
<Card sx={{width: "100%"}}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box py={2}>
<Typography variant="h6" fontWeight="medium" p={3} display="inline">
{props.label}
</Typography>
{
props.labelAdditionalComponentsLeft.map((component, i) =>
{
return (<span key={i}>{renderComponent(component)}</span>);
})
}
</Box>
<Box pr={1}>
{
props.labelAdditionalComponentsRight.map((component, i) =>
{
return (<span key={i}>{renderComponent(component)}</span>);
})
}
</Box>
</Box>
{props.children}
</Card>
{
showEditForm &&
<Modal open={showEditForm as boolean} onClose={(event, reason) => closeEditForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditForm}
table={showEditForm.table}
id={showEditForm.id}
defaultValues={showEditForm.defaultValues}
disabledFields={showEditForm.disabledFields} />
</div>
</Modal>
}
</>
);
}
export default Widget;

View File

@ -273,9 +273,9 @@ function EntityView({table, launchProcess}: Props): JSX.Element
{ {
sectionFieldElements.set(section.name, sectionFieldElements.set(section.name,
<Grid id={section.name} key={section.name} item lg={12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}> <Grid id={section.name} key={section.name} item lg={12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
<MDBox mb={3} width="100%"> <MDBox width="100%">
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}}> <Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}}>
<MDTypography variant="h5" p={3} pb={1}> <MDTypography variant="h6" p={3} pb={1}>
{section.label} {section.label}
</MDTypography> </MDTypography>
<MDBox p={3} pt={0} flexDirection="column"> <MDBox p={3} pt={0} flexDirection="column">
@ -466,9 +466,9 @@ function EntityView({table, launchProcess}: Props): JSX.Element
{nonT1TableSections.length > 0 ? nonT1TableSections.map(({ {nonT1TableSections.length > 0 ? nonT1TableSections.map(({
iconName, label, name, fieldNames, tier, iconName, label, name, fieldNames, tier,
}: any) => ( }: any) => (
<> <React.Fragment key={name}>
{sectionFieldElements.get(name)} {sectionFieldElements.get(name)}
</> </React.Fragment>
)) : null} )) : null}
</Grid> </Grid>
<MDBox component="form" p={3}> <MDBox component="form" p={3}>

View File

@ -275,7 +275,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
<Grid m={3} mt={9} container> <Grid m={3} mt={9} container>
<Grid item xs={0} lg={3} /> <Grid item xs={0} lg={3} />
<Grid item xs={12} lg={6}> <Grid item xs={12} lg={6}>
<Card> <Card elevation={5}>
<MDBox p={3}> <MDBox p={3}>
<MDTypography variant="h5" component="div"> <MDTypography variant="h5" component="div">
Working Working