From e7d870a7fa0c16c7f66c1cb3d85a936a7569ea70 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 29 Apr 2024 12:52:29 -0500 Subject: [PATCH 1/4] CE-1068 - Add dumping console logs upon error - could help diagnose test fails faster hopefully --- .../selenium/lib/QBaseSeleniumTest.java | 12 +- .../selenium/lib/QSeleniumLib.java | 20 +++ .../selenium/lib/SeleniumTestWatcher.java | 126 ++++++++++++++++++ 3 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/SeleniumTestWatcher.java diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QBaseSeleniumTest.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QBaseSeleniumTest.java index 57ed9c1..5d6e058 100755 --- a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QBaseSeleniumTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QBaseSeleniumTest.java @@ -33,6 +33,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.Dimension; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; @@ -43,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* ** Base class for Selenium tests *******************************************************************************/ +@ExtendWith(SeleniumTestWatcher.class) public class QBaseSeleniumTest { protected static ChromeOptions chromeOptions; @@ -93,6 +95,8 @@ public class QBaseSeleniumTest driver.manage().window().setSize(new Dimension(1700, 1300)); qSeleniumLib = new QSeleniumLib(driver); + SeleniumTestWatcher.setCurrentSeleniumLib(qSeleniumLib); + if(useInternalJavalin()) { qSeleniumJavalin = new QSeleniumJavalin(); @@ -197,10 +201,10 @@ public class QBaseSeleniumTest qSeleniumLib.takeScreenshotToFile(getClass().getSimpleName() + "/" + testInfo.getDisplayName()); } - if(driver != null) - { - driver.quit(); - } + //////////////////////////////////////////////////////////////////////////////////////// + // note - at one time we did a driver.quit here - but we're moving that into // + // SeleniumTestWatcher, so it can dump logs if it wants to (it runs after the @After) // + //////////////////////////////////////////////////////////////////////////////////////// if(qSeleniumJavalin != null) { diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java index 18a4f98..9671353 100755 --- a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java +++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java @@ -42,6 +42,8 @@ import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import static org.assertj.core.api.Assertions.assertThat; @@ -735,4 +737,22 @@ public class QSeleniumLib return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void dumpConsole() + { + Set availableLogTypes = driver.manage().logs().getAvailableLogTypes(); + for(String logType : availableLogTypes) + { + LogEntries logEntries = driver.manage().logs().get(logType); + for(LogEntry logEntry : logEntries) + { + System.out.println(logEntry.toJson()); + } + } + } + } diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/SeleniumTestWatcher.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/SeleniumTestWatcher.java new file mode 100644 index 0000000..0aa34b5 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/SeleniumTestWatcher.java @@ -0,0 +1,126 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.frontend.materialdashboard.selenium.lib; + + +import java.util.Optional; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SeleniumTestWatcher implements TestWatcher +{ + private static QSeleniumLib qSeleniumLib; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void setCurrentSeleniumLib(QSeleniumLib qSeleniumLib) + { + SeleniumTestWatcher.qSeleniumLib = qSeleniumLib; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void testFailed(ExtensionContext context, Throwable cause) + { + if(qSeleniumLib != null) + { + System.out.println("Dumping browser console after failed test: " + context.getDisplayName()); + System.out.println("----------------------------------------------------------------------------"); + try + { + qSeleniumLib.dumpConsole(); + } + catch(Exception e) + { + System.out.println("Error dumping console:"); + e.printStackTrace(); + } + System.out.println("----------------------------------------------------------------------------"); + } + + tryToQuitSelenium(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void tryToQuitSelenium() + { + if(qSeleniumLib != null) + { + try + { + qSeleniumLib.driver.quit(); + } + catch(Exception e) + { + System.err.println("Error quiting selenium driver: " + e.getMessage()); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void testSuccessful(ExtensionContext context) + { + tryToQuitSelenium(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void testAborted(ExtensionContext context, Throwable cause) + { + tryToQuitSelenium(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void testDisabled(ExtensionContext context, Optional reason) + { + tryToQuitSelenium(); + } +} From 8707aa8a940ee17a7b9c91d4b7f782d5c3046f15 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 30 Apr 2024 10:06:21 -0500 Subject: [PATCH 2/4] CE-1179: checkpoint commit for integrations --- package.json | 2 +- .../components/query/AssignFilterVariable.tsx | 66 +++++++++++ .../query/BasicAndAdvancedQueryControls.tsx | 4 +- .../components/query/CriteriaDateField.tsx | 37 ++++--- .../components/query/CustomColumnsPanel.tsx | 16 +-- .../components/query/EvaluatedExpression.tsx | 7 +- .../components/query/FilterCriteriaRow.tsx | 33 +++--- .../query/FilterCriteriaRowValues.tsx | 103 +++++++++++++----- src/qqq/components/query/QuickFilter.tsx | 78 ++++++------- src/qqq/components/widgets/Widget.tsx | 9 +- .../widgets/misc/PivotTableSetupWidget.tsx | 14 +-- .../widgets/misc/ReportSetupWidget.tsx | 42 +++---- src/qqq/pages/records/query/RecordQuery.tsx | 34 +++--- src/qqq/utils/qqq/FilterUtils.tsx | 10 +- 14 files changed, 295 insertions(+), 160 deletions(-) create mode 100644 src/qqq/components/query/AssignFilterVariable.tsx diff --git a/package.json b/package.json index cea87a5..4d4a2c9 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.96", + "@kingsrook/qqq-frontend-core": "1.0.98", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/query/AssignFilterVariable.tsx b/src/qqq/components/query/AssignFilterVariable.tsx new file mode 100644 index 0000000..642f157 --- /dev/null +++ b/src/qqq/components/query/AssignFilterVariable.tsx @@ -0,0 +1,66 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {FilterVariableExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/FilterVariableExpression"; +import Box from "@mui/material/Box"; +import Icon from "@mui/material/Icon"; +import Tooltip from "@mui/material/Tooltip"; +import CriteriaDateField from "qqq/components/query/CriteriaDateField"; +import React, {SyntheticEvent, useState} from "react"; + + +export type Expression = FilterVariableExpression; + + +interface AssignFilterButtonProps +{ + valueIndex: number; + field: QFieldMetaData; + valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; +} + +CriteriaDateField.defaultProps = { + valueIndex: 0, + label: "Value", + idPrefix: "value-" +}; + +export default function AssignFilterVariable({valueIndex, field, valueChangeHandler}: AssignFilterButtonProps): JSX.Element +{ + const [isValueAVariable, setIsValueAVariable] = useState(false); + + const handleVariableButtonOnClick = () => + { + setIsValueAVariable(!isValueAVariable); + const expression = new FilterVariableExpression({fieldName: field.name, valueIndex: valueIndex}); + valueChangeHandler(null, valueIndex, expression); + }; + + return + + + functions + + + ; +} + diff --git a/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx b/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx index 7c13dff..2f45a95 100644 --- a/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx +++ b/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx @@ -114,7 +114,7 @@ export function getCurrentSortIndicator(queryFilter: QQueryFilter, tableMetaData *******************************************************************************/ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryControlsProps, ref) => { - const {metaData, tableMetaData, savedViewsComponent, columnMenuComponent, quickFilterFieldNames, setQuickFilterFieldNames, setQueryFilter, queryFilter, gridApiRef, queryFilterJSON, mode, setMode} = props; + const {metaData, tableMetaData, savedViewsComponent, columnMenuComponent, quickFilterFieldNames, setQuickFilterFieldNames, setQueryFilter, queryFilter, gridApiRef, queryFilterJSON, mode, setMode, queryScreenUsage} = props; ///////////////////// // state variables // @@ -682,6 +682,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo criteriaParam={getQuickCriteriaParam(fieldName)} fieldMetaData={field} defaultOperator={defaultOperator} + queryScreenUsage={queryScreenUsage} handleRemoveQuickFilterField={null} />); }) } @@ -701,6 +702,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo criteriaParam={getQuickCriteriaParam(fieldName)} fieldMetaData={field} defaultOperator={defaultOperator} + queryScreenUsage={queryScreenUsage} handleRemoveQuickFilterField={handleRemoveQuickFilterField} />); }) } diff --git a/src/qqq/components/query/CriteriaDateField.tsx b/src/qqq/components/query/CriteriaDateField.tsx index 7040c5d..1c61f5d 100644 --- a/src/qqq/components/query/CriteriaDateField.tsx +++ b/src/qqq/components/query/CriteriaDateField.tsx @@ -21,6 +21,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {FilterVariableExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/FilterVariableExpression"; import {NowExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowExpression"; import {NowWithOffsetExpression, NowWithOffsetOperator, NowWithOffsetUnit} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression"; import {ThisOrLastPeriodExpression, ThisOrLastPeriodOperator, ThisOrLastPeriodUnit} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression"; @@ -34,14 +35,14 @@ import MenuItem from "@mui/material/MenuItem"; import {styled} from "@mui/material/styles"; import TextField from "@mui/material/TextField"; import Tooltip, {tooltipClasses, TooltipProps} from "@mui/material/Tooltip"; -import React, {SyntheticEvent, useEffect, useReducer, useState} from "react"; import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; import {EvaluatedExpression} from "qqq/components/query/EvaluatedExpression"; import {makeTextField} from "qqq/components/query/FilterCriteriaRowValues"; +import React, {SyntheticEvent, useReducer, useState} from "react"; -export type Expression = NowWithOffsetExpression | ThisOrLastPeriodExpression | NowExpression; +export type Expression = NowWithOffsetExpression | ThisOrLastPeriodExpression | NowExpression | FilterVariableExpression; interface CriteriaDateFieldProps @@ -52,6 +53,7 @@ interface CriteriaDateFieldProps field: QFieldMetaData; criteria: QFilterCriteriaWithId; valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; + allowVariables?: boolean; } CriteriaDateField.defaultProps = { @@ -60,19 +62,30 @@ CriteriaDateField.defaultProps = { idPrefix: "value-" }; -export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler}: CriteriaDateFieldProps): JSX.Element +export const NoWrapTooltip = styled(({className, children, ...props}: TooltipProps) => ( + {children} +))({ + [`& .${tooltipClasses.tooltip}`]: { + whiteSpace: "nowrap" + }, +}); + +export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler, allowVariables}: CriteriaDateFieldProps): JSX.Element { + const [relativeDateTimeOpen, setRelativeDateTimeOpen] = useState(false); const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); - const [forceAdvancedDateTimeDialogOpen, setForceAdvancedDateTimeDialogOpen] = useState(false) + const [forceAdvancedDateTimeDialogOpen, setForceAdvancedDateTimeDialogOpen] = useState(false); const [, forceUpdate] = useReducer((x) => x + 1, 0); const openRelativeDateTimeMenu = (event: React.MouseEvent) => { + setRelativeDateTimeOpen(true); setRelativeDateTimeMenuAnchorElement(event.currentTarget); }; const closeRelativeDateTimeMenu = () => { + setRelativeDateTimeOpen(false); setRelativeDateTimeMenuAnchorElement(null); }; @@ -137,20 +150,12 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; const currentExpression = isExpression ? criteria.values[valueIndex] : null; - const NoWrapTooltip = styled(({className, children, ...props}: TooltipProps) => ( - {children} - ))({ - [`& .${tooltipClasses.tooltip}`]: { - whiteSpace: "nowrap" - }, - }); - const tooltipMenuItemFromExpression = (valueIndex: number, tooltipPlacement: "left" | "right", expression: Expression) => { let startOfPrefix = ""; - if(expression.type == "ThisOrLastPeriod") + if (expression.type == "ThisOrLastPeriod") { - if(field.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS") + if (field.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS") { startOfPrefix = "start of "; } @@ -194,14 +199,14 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c return { isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) - : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix) + : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix, allowVariables) } date_range ( const someRef = createRef(); const textRef = useRef(null); - const [didInitialFocus, setDidInitialFocus] = useState(false) + const [didInitialFocus, setDidInitialFocus] = useState(false); const [openGroups, setOpenGroups] = useState(props.initialOpenedGroups || {}); const openGroupsBecauseOfFilter = {} as { [name: string]: boolean }; @@ -71,9 +73,9 @@ export const CustomColumnsPanel = forwardRef( console.log(`Open groups: ${JSON.stringify(openGroups)}`); - if(!didInitialFocus) + if (!didInitialFocus) { - if(textRef.current) + if (textRef.current) { textRef.current.select(); setDidInitialFocus(true); @@ -189,11 +191,11 @@ export const CustomColumnsPanel = forwardRef( /////////////////////////////////////////////////////////////////////////////////////////////////////// // always sort columns by label. note, in future may offer different sorts - here's where to do it. // /////////////////////////////////////////////////////////////////////////////////////////////////////// - const sortedColumns = [... columns]; + const sortedColumns = [...columns]; sortedColumns.sort((a, b): number => { return a.headerName.localeCompare(b.headerName); - }) + }); for (let i = 0; i < sortedColumns.length; i++) { @@ -361,7 +363,7 @@ export const CustomColumnsPanel = forwardRef( const changeFilterText = (newValue: string) => { setFilterText(newValue); - props.filterTextChanger(newValue) + props.filterTextChanger(newValue); }; const filterTextChanged = (event: React.ChangeEvent) => diff --git a/src/qqq/components/query/EvaluatedExpression.tsx b/src/qqq/components/query/EvaluatedExpression.tsx index 9ab4f53..23c3065 100644 --- a/src/qqq/components/query/EvaluatedExpression.tsx +++ b/src/qqq/components/query/EvaluatedExpression.tsx @@ -21,9 +21,9 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; -import React, {useEffect, useState} from "react"; import {Expression} from "qqq/components/query/CriteriaDateField"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React, {useEffect, useState} from "react"; /******************************************************************************* ** Helper component to show value inside tooltips that ticks up every second. @@ -57,6 +57,11 @@ const HOUR_MS = 60 * 60 * 1000; const DAY_MS = 24 * 60 * 60 * 1000; const evaluateExpression = (time: Date, field: QFieldMetaData, expression: Expression): string => { + if (expression.type == "FilterVariableExpression") + { + return (expression.toString()); + } + let rs: Date = null; if (expression.type == "NowWithOffset") { diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index ff720a2..2f82085 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -72,7 +72,7 @@ export const getValueModeRequiredCount = (valueMode: ValueMode): number => case ValueMode.PVS_MULTI: return (null); } -} +}; export interface OperatorOption { @@ -183,7 +183,7 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str } return (operatorOptions); -} +}; interface FilterCriteriaRowProps @@ -200,10 +200,9 @@ interface FilterCriteriaRowProps } FilterCriteriaRow.defaultProps = - { - }; + {}; -export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValue?: OperatorOption): {criteriaIsValid: boolean, criteriaStatusTooltip: string} +export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValue?: OperatorOption): { criteriaIsValid: boolean, criteriaStatusTooltip: string } { let criteriaIsValid = true; let criteriaStatusTooltip = "This condition is fully defined and is part of your filter."; @@ -213,7 +212,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu return (value === null || value == undefined || String(value).trim() === ""); } - if(!criteria) + if (!criteria) { criteriaIsValid = false; criteriaStatusTooltip = "This condition is not defined."; @@ -284,7 +283,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, let defaultFieldValue; let field = null; let fieldTable = null; - if(criteria && criteria.fieldName) + if (criteria && criteria.fieldName) { [field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName); if (field && fieldTable) @@ -303,9 +302,9 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, let newOperatorSelectedValue = operatorOptions.filter(option => { - if(option.value == criteria.operator) + if (option.value == criteria.operator) { - if(option.implicitValues) + if (option.implicitValues) { return (JSON.stringify(option.implicitValues) == JSON.stringify(criteria.values)); } @@ -316,7 +315,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, } return (false); })[0]; - if(newOperatorSelectedValue?.label !== operatorSelectedValue?.label) + if (newOperatorSelectedValue?.label !== operatorSelectedValue?.label) { setOperatorSelectedValue(newOperatorSelectedValue); setOperatorInputValue(newOperatorSelectedValue?.label); @@ -379,12 +378,12 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, { criteria.operator = newValue ? newValue.value : null; - if(newValue) + if (newValue) { setOperatorSelectedValue(newValue); setOperatorInputValue(newValue.label); - if(newValue.implicitValues) + if (newValue.implicitValues) { criteria.values = newValue.implicitValues; } @@ -393,15 +392,15 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, // we've seen cases where switching operators can sometimes put a null in as the first value... // // that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. // ////////////////////////////////////////////////////////////////////////////////////////////////// - if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null) + if (criteria.values && criteria.values.length == 1 && criteria.values[0] == null) { criteria.values = []; } - if(newValue.valueMode && !newValue.implicitValues) + if (newValue.valueMode && !newValue.implicitValues) { const requiredValueCount = getValueModeRequiredCount(newValue.valueMode); - if(requiredValueCount != null && criteria.values.length > requiredValueCount) + if (requiredValueCount != null && criteria.values.length > requiredValueCount) { criteria.values.splice(requiredValueCount); } @@ -424,12 +423,12 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, // @ts-ignore const value = newValue !== undefined ? newValue : event ? event.target.value : null; - if(!criteria.values) + if (!criteria.values) { criteria.values = []; } - if(valueIndex == "all") + if (valueIndex == "all") { criteria.values = value; } diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index 38f0e66..5e5a653 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -23,19 +23,23 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {FilterVariableExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/FilterVariableExpression"; import Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; import Icon from "@mui/material/Icon"; import IconButton from "@mui/material/IconButton"; import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; import TextField from "@mui/material/TextField"; -import React, {SyntheticEvent, useReducer} from "react"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; -import CriteriaDateField from "qqq/components/query/CriteriaDateField"; +import AssignFilterVariable from "qqq/components/query/AssignFilterVariable"; +import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; +import {EvaluatedExpression} from "qqq/components/query/EvaluatedExpression"; import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster"; import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; +import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React, {SyntheticEvent, useReducer, useState} from "react"; interface Props { @@ -44,7 +48,8 @@ interface Props field: QFieldMetaData; table: QTableMetaData; valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; - initiallyOpenMultiValuePvs?: boolean + initiallyOpenMultiValuePvs?: boolean; + queryScreenUsage?: QueryScreenUsage; } FilterCriteriaRowValues.defaultProps = @@ -72,8 +77,10 @@ export const getTypeForTextField = (field: QFieldMetaData): string => return (type); }; -export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-") => +export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) => { + const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; + let type = getTypeForTextField(field); const inputLabelProps: any = {}; @@ -95,7 +102,6 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi }; - /******************************************************************************* ** Event handler for key-down events - specifically added here, to stop pressing ** 'tab' in a date or date-time from closing the quick-filter... @@ -104,7 +110,7 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi { if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME) { - if(e.code == "Tab") + if (e.code == "Tab") { console.log("Tab on date or date-time - don't close me, just move to the next sub-field!..."); e.stopPropagation(); @@ -112,6 +118,36 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi } }; + + const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => + { + valueChangeHandler(event, index, ""); + document.getElementById(`${idPrefix}${criteria.id}`).focus(); + }; + + const inputProps2: any = {}; + inputProps2.endAdornment = ( + + clearValue(event, valueIndex)}> + closer + + + ); + + return } placement="bottom" enterDelay={1000} sx={{marginLeft: "-75px !important", marginTop: "-8px !important"}}>; + }; + const inputProps: any = {}; inputProps.endAdornment = ( @@ -121,25 +157,40 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi ); - return valueChangeHandler(event, valueIndex)} - onKeyDown={handleKeyDown} - value={value} - InputLabelProps={inputLabelProps} - InputProps={inputProps} - fullWidth - autoFocus={true} - />; + return + { + isExpression ? ( + makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) + ) : ( + valueChangeHandler(event, valueIndex)} + onKeyDown={handleKeyDown} + value={value} + InputLabelProps={inputLabelProps} + InputProps={inputProps} + fullWidth + autoFocus={true} + /> + ) + } + { + allowVariables && ( + + ) + } + ; }; -function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs}: Props): JSX.Element + +function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage}: Props): JSX.Element { const [, forceUpdate] = useReducer((x) => x + 1, 0); + const [allowVariables, setAllowVariables] = useState(queryScreenUsage == "reportSetup"); if (!operatorOption) { @@ -174,7 +225,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC case ValueMode.NONE: return null; case ValueMode.SINGLE: - return makeTextField(field, criteria, valueChangeHandler); + return makeTextField(field, criteria, valueChangeHandler, 0, undefined, undefined, allowVariables); case ValueMode.SINGLE_DATE: return ; case ValueMode.DOUBLE_DATE: @@ -183,7 +234,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC ; case ValueMode.SINGLE_DATE_TIME: - return ; + return ; case ValueMode.DOUBLE_DATE_TIME: return @@ -192,10 +243,10 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC case ValueMode.DOUBLE: return - {makeTextField(field, criteria, valueChangeHandler, 0, "From", "from-")} + {makeTextField(field, criteria, valueChangeHandler, 0, "From", "from-", allowVariables)} - {makeTextField(field, criteria, valueChangeHandler, 1, "To", "to-")} + {makeTextField(field, criteria, valueChangeHandler, 1, "To", "to-", allowVariables)} ; case ValueMode.MULTI: @@ -276,4 +327,4 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC return (
); } -export default FilterCriteriaRowValues; \ No newline at end of file +export default FilterCriteriaRowValues; diff --git a/src/qqq/components/query/QuickFilter.tsx b/src/qqq/components/query/QuickFilter.tsx index 3a3362b..9de8049 100644 --- a/src/qqq/components/query/QuickFilter.tsx +++ b/src/qqq/components/query/QuickFilter.tsx @@ -30,14 +30,15 @@ import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Menu from "@mui/material/Menu"; import TextField from "@mui/material/TextField"; -import React, {SyntheticEvent, useContext, useReducer, useState} from "react"; import QContext from "QContext"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow"; import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues"; import XIcon from "qqq/components/query/XIcon"; +import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import FilterUtils from "qqq/utils/qqq/FilterUtils"; import TableUtils from "qqq/utils/qqq/TableUtils"; +import React, {SyntheticEvent, useContext, useReducer, useState} from "react"; export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex"; @@ -50,6 +51,7 @@ interface QuickFilterProps updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean, doRemoveCriteria: boolean) => void; defaultOperator?: QCriteriaOperator; handleRemoveQuickFilterField?: (fieldName: string) => void; + queryScreenUsage?: QueryScreenUsage; } QuickFilter.defaultProps = @@ -71,7 +73,7 @@ export const quickFilterButtonStyles = { minHeight: "auto", padding: "0.375rem 0.625rem", whiteSpace: "nowrap", marginBottom: "0.5rem" -} +}; /******************************************************************************* ** Test if a CriteriaParamType represents an actual query criteria - or, if it's @@ -89,11 +91,11 @@ const criteriaParamIsCriteria = (param: CriteriaParamType): boolean => *******************************************************************************/ const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteria: QFilterCriteriaWithId): boolean => { - if(operatorOption.value == criteria.operator) + if (operatorOption.value == criteria.operator) { - if(operatorOption.implicitValues) + if (operatorOption.implicitValues) { - if(JSON.stringify(operatorOption.implicitValues) == JSON.stringify(criteria.values)) + if (JSON.stringify(operatorOption.implicitValues) == JSON.stringify(criteria.values)) { return (true); } @@ -107,7 +109,7 @@ const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteri } return (false); -} +}; /******************************************************************************* @@ -117,29 +119,29 @@ const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteri *******************************************************************************/ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption => { - if(criteria) + if (criteria) { const filteredOptions = operatorOptions.filter(o => doesOperatorOptionEqualCriteria(o, criteria)); - if(filteredOptions.length > 0) + if (filteredOptions.length > 0) { return (filteredOptions[0]); } } const filteredOptions = operatorOptions.filter(o => o.value == defaultOperator); - if(filteredOptions.length > 0) + if (filteredOptions.length > 0) { return (filteredOptions[0]); } return (null); -} +}; /******************************************************************************* ** Component to render a QuickFilter - that is - a button, with a Menu under it, ** with Operator and Value controls. *******************************************************************************/ -export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField}: QuickFilterProps): JSX.Element +export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage}: QuickFilterProps): JSX.Element { const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : []; const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName); @@ -190,7 +192,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData ////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria)) { - if(isOpen) + if (isOpen) { //////////////////////////////////////////////////////////////////////////////// // this was firing too-often for case where: there was a criteria originally // @@ -217,12 +219,12 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData *******************************************************************************/ const criteriaNeedsReset = (): boolean => { - if(criteria != null && criteriaParam == null) + if (criteria != null && criteriaParam == null) { const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0]; - if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue())) + if (criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue())) { - if(isOpen) + if (isOpen) { ////////////////////////////////////////////////////////////////////////////////// // this was firing too-often for case where: there was no criteria originally, // @@ -237,7 +239,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData } return (false); - } + }; /******************************************************************************* ** Construct a new criteria object - resetting the values tied to the operator @@ -251,8 +253,8 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData setOperatorSelectedValue(operatorOption); setOperatorInputValue(operatorOption?.label); setCriteria(criteria); - return(criteria); - } + return (criteria); + }; /******************************************************************************* ** event handler to open the menu in response to the button being clicked. @@ -266,7 +268,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData { const element = document.getElementById("value-" + criteria.id); element?.focus(); - }) + }); }; /******************************************************************************* @@ -304,15 +306,15 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData // we've seen cases where switching operators can sometimes put a null in as the first value... // // that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. // ////////////////////////////////////////////////////////////////////////////////////////////////// - if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null) + if (criteria.values && criteria.values.length == 1 && criteria.values[0] == null) { criteria.values = []; } - if(newValue.valueMode && !newValue.implicitValues) + if (newValue.valueMode && !newValue.implicitValues) { const requiredValueCount = getValueModeRequiredCount(newValue.valueMode); - if(requiredValueCount != null && criteria.values.length > requiredValueCount) + if (requiredValueCount != null && criteria.values.length > requiredValueCount) { criteria.values.splice(requiredValueCount); } @@ -345,6 +347,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData // @ts-ignore const value = newValue !== undefined ? newValue : event ? event.target.value : null; + console.log("IN HERE"); if (!criteria.values) { criteria.values = []; @@ -376,13 +379,13 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData *******************************************************************************/ const resetCriteria = (e: React.MouseEvent) => { - if(criteriaIsValid) + if (criteriaIsValid) { e.stopPropagation(); const newCriteria = makeNewCriteria(); updateCriteria(newCriteria, false, true); } - } + }; /******************************************************************************* ** event handler for clicking the (x) icon that turns off this quick filter field. @@ -390,17 +393,17 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData *******************************************************************************/ const handleTurningOffQuickFilterField = () => { - closeMenu() - if(handleRemoveQuickFilterField) + closeMenu(); + if (handleRemoveQuickFilterField) { handleRemoveQuickFilterField(criteria?.fieldName); } - } + }; //////////////////////////////////////////////////////////////////////////////////// // if no field was input (e.g., record-query is still loading), return null early // //////////////////////////////////////////////////////////////////////////////////// - if(!fieldMetaData) + if (!fieldMetaData) { return (null); } @@ -410,10 +413,10 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData // from the last selected one, then set the state vars that control that autocomplete // ////////////////////////////////////////////////////////////////////////////////////////// const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator); - if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue)) + if (JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue)) { - setOperatorSelectedValue(maybeNewOperatorSelectedValue) - setOperatorInputValue(maybeNewOperatorSelectedValue?.label) + setOperatorSelectedValue(maybeNewOperatorSelectedValue); + setOperatorInputValue(maybeNewOperatorSelectedValue?.label); } ///////////////////////////////////////////////////////////////////////////////////// @@ -431,7 +434,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData const tooltipEnterDelay = 500; let buttonAdditionalStyles: any = {}; - let buttonContent = {tableForField?.name != tableMetaData.name ? `${tableForField.label}: ` : ""}{fieldMetaData.label} + let buttonContent = {tableForField?.name != tableMetaData.name ? `${tableForField.label}: ` : ""}{fieldMetaData.label}; let buttonClassName = "filterNotActive"; if (criteriaIsValid) { @@ -446,9 +449,9 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData // don't show the Equals or In operators // /////////////////////////////////////////// let operatorString = (<>{operatorSelectedValue.label} ); - if(operatorSelectedValue.value == QCriteriaOperator.EQUALS || operatorSelectedValue.value == QCriteriaOperator.IN) + if (operatorSelectedValue.value == QCriteriaOperator.EQUALS || operatorSelectedValue.value == QCriteriaOperator.IN) { - operatorString = (<>) + operatorString = (<>); } buttonContent = (<>{buttonContent}: {operatorString}{valuesString}); @@ -491,7 +494,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData const xClicked = (e: React.MouseEvent) => { e.stopPropagation(); - if(criteriaIsValid) + if (criteriaIsValid) { resetCriteria(e); } @@ -499,12 +502,12 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData { handleTurningOffQuickFilterField(); } - } + }; ////////////////////////////// // return the button & menu // ////////////////////////////// - const widthAndMaxWidth = fieldMetaData?.type == QFieldType.DATE_TIME ? 275 : 250 + const widthAndMaxWidth = (fieldMetaData?.type == QFieldType.DATE_TIME) ? 295 : 250; return ( <> {button} @@ -541,6 +544,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
{ onClickCallback(); - } + }; return ( @@ -236,7 +234,6 @@ export function HeaderToggleComponent({label, getValue, onClickCallback, disable } - /******************************************************************************* ** *******************************************************************************/ @@ -698,7 +695,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element ); let sublabelElement = ( - + {props.widgetData?.sublabel} @@ -785,7 +782,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element } {localLabelAdditionalElementsLeft} - + { hasPermission && props.widgetData?.sublabel && (sublabelElement) } diff --git a/src/qqq/components/widgets/misc/PivotTableSetupWidget.tsx b/src/qqq/components/widgets/misc/PivotTableSetupWidget.tsx index 7b243e4..9c22a98 100644 --- a/src/qqq/components/widgets/misc/PivotTableSetupWidget.tsx +++ b/src/qqq/components/widgets/misc/PivotTableSetupWidget.tsx @@ -280,7 +280,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor } modalPivotTableDefinition[rowsOrColumns].push(new PivotTableGroupBy()); - validateForm() + validateForm(); forceUpdate(); } @@ -292,7 +292,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor { updateUsedGroupByFieldNames(modalPivotTableDefinition); updateUsedValueFieldNames(modalPivotTableDefinition); - validateForm() + validateForm(); forceUpdate(); } @@ -308,7 +308,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor } modalPivotTableDefinition.values.push(new PivotTableValue()); - validateForm() + validateForm(); forceUpdate(); } @@ -319,7 +319,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor function removeValue(index: number) { modalPivotTableDefinition.values.splice(index, 1); - validateForm() + validateForm(); forceUpdate(); } @@ -503,7 +503,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor const labelAdditionalElementsRight: JSX.Element[] = []; if (isEditable) { - labelAdditionalElementsRight.push( enabled} onClickCallback={toggleEnabled} />); + labelAdditionalElementsRight.push( enabled} onClickCallback={toggleEnabled} />); } @@ -659,7 +659,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor // if this isn't a call from the on-submit handler, and we haven't previously attempted a submit, then return w/o setting any alerts // // this is like a version of considering "touched"... // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(!submitting && !attemptedSubmit) + if (!submitting && !attemptedSubmit) { return; } @@ -703,7 +703,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor // now they've fixed 'em - so go back to a 'clean' state - so if they add more // // boxes, they won't immediately show errors, until a re-submit // //////////////////////////////////////////////////////////////////////////////////// - if(attemptedSubmit) + if (attemptedSubmit) { setAttemptedSubmit(false); } diff --git a/src/qqq/components/widgets/misc/ReportSetupWidget.tsx b/src/qqq/components/widgets/misc/ReportSetupWidget.tsx index 3466088..05ebfa2 100644 --- a/src/qqq/components/widgets/misc/ReportSetupWidget.tsx +++ b/src/qqq/components/widgets/misc/ReportSetupWidget.tsx @@ -46,8 +46,8 @@ interface ReportSetupWidgetProps { isEditable: boolean; widgetMetaData: QWidgetMetaData; - recordValues: {[name: string]: any}; - onSaveCallback?: (values: {[name: string]: any}) => void; + recordValues: { [name: string]: any }; + onSaveCallback?: (values: { [name: string]: any }) => void; } ReportSetupWidget.defaultProps = { @@ -103,14 +103,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal ///////////////////////////// let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter; let usingDefaultEmptyFilter = false; - if(!queryFilter) + if (!queryFilter) { queryFilter = new QQueryFilter(); usingDefaultEmptyFilter = true; } let columns: QQueryColumns = null; - if(recordValues["columnsJson"]) + if (recordValues["columnsJson"]) { columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]); } @@ -124,12 +124,12 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal { (async () => { - const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]) + const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]); setTableMetaData(tableMetaData); const queryFilterForFrontend = Object.assign({}, queryFilter); - await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend) - setFrontendQueryFilter(queryFilterForFrontend) + await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend); + setFrontendQueryFilter(queryFilterForFrontend); })(); } }, [recordValues]); @@ -140,7 +140,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function openEditor() { - if(recordValues["tableName"]) + if (recordValues["tableName"]) { setModalOpen(true); } @@ -152,7 +152,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function saveClicked() { - if(!onSaveCallback) + if (!onSaveCallback) { console.log("onSaveCallback was not defined"); return; @@ -181,7 +181,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function closeEditor(event?: {}, reason?: "backdropClick" | "escapeKeyDown") { - if(reason == "backdropClick" || reason == "escapeKeyDown") + if (reason == "backdropClick" || reason == "escapeKeyDown") { return; } @@ -195,9 +195,9 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function renderColumn(column: Column): JSX.Element { - const [field, table] = FilterUtils.getField(tableMetaData, column.name) + const [field, table] = FilterUtils.getField(tableMetaData, column.name); - if(!column || !column.isVisible || column.name == "__check__" || !field) + if (!column || !column.isVisible || column.name == "__check__" || !field) { return (); } @@ -215,9 +215,9 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function mayShowQueryPreview(): boolean { - if(tableMetaData) + if (tableMetaData) { - if(frontendQueryFilter?.criteria?.length > 0 || frontendQueryFilter?.subFilters?.length > 0) + if (frontendQueryFilter?.criteria?.length > 0 || frontendQueryFilter?.subFilters?.length > 0) { return (true); } @@ -231,11 +231,11 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal *******************************************************************************/ function mayShowColumnsPreview(): boolean { - if(tableMetaData) + if (tableMetaData) { - for(let i = 0; i) + labelAdditionalElementsRight.push(); } @@ -316,7 +316,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal { mayShowColumnsPreview() && - columns.columns.map((column, i) => {renderColumn(column)}) + columns.columns.map((column, i) => {renderColumn(column)}) } { !mayShowColumnsPreview() && diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 0b3fd57..25609ec 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -109,7 +109,7 @@ const qController = Client.getInstance(); *******************************************************************************/ const getLoadingScreen = (isModal: boolean) => { - if(isModal) + if (isModal) { return ( ); } @@ -151,7 +151,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init *******************************************************************************/ function localStorageSet(key: string, value: string) { - if(mayWriteLocalStorage) + if (mayWriteLocalStorage) { localStorage.setItem(key, value); } @@ -163,7 +163,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init *******************************************************************************/ function localStorageRemove(key: string) { - if(mayWriteLocalStorage) + if (mayWriteLocalStorage) { localStorage.removeItem(key); } @@ -176,7 +176,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init { return view; } - } + }; }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -256,7 +256,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init defaultView.mode = defaultMode; } - if(firstRender) + if (firstRender) { ///////////////////////////////////////////////////////////////////////// // allow a caller to send in an initial filter & set of columns. // @@ -408,7 +408,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init ////////////////////////////////////////////////////////////////// // we use our own header - so clear out the context page header // ////////////////////////////////////////////////////////////////// - if(!isModal) + if (!isModal) { setPageHeader(null); } @@ -486,7 +486,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init }; - /******************************************************************************* ** *******************************************************************************/ @@ -711,7 +710,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init { if (localStorage.getItem(currentSavedViewLocalStorageKey)) { - if(usage == "queryScreen") + if (usage == "queryScreen") { currentSavedViewId = Number.parseInt(localStorage.getItem(currentSavedViewLocalStorageKey)); navigate(`${metaData.getTablePathByName(tableName)}/savedView/${currentSavedViewId}`); @@ -750,13 +749,13 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init const viewForLocalStorage: RecordQueryView = JSON.parse(viewAsJSON); if (viewForLocalStorage?.queryFilter?.criteria?.length > 0) { - FilterUtils.stripAwayIncompleteCriteria(viewForLocalStorage.queryFilter) + FilterUtils.stripAwayIncompleteCriteria(viewForLocalStorage.queryFilter); } localStorageSet(viewLocalStorageKey, JSON.stringify(viewForLocalStorage)); } - catch(e) + catch (e) { - console.log("Error storing view in local storage: " + e) + console.log("Error storing view in local storage: " + e); } }; @@ -939,7 +938,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init console.log(`Issuing query: ${thisQueryId}`); if (tableMetaData.capabilities.has(Capability.TABLE_COUNT)) { - if(clearOutCount) + if (clearOutCount) { setTotalRecords(null); setDistinctRecords(null); @@ -1437,7 +1436,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init return (selectedIds.length); }; - /******************************************************************************* ** get a query-string to put on the url to indicate what records are going into ** a process. @@ -2527,7 +2525,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init { const currentSavedViewId = Number.parseInt(localStorage.getItem(currentSavedViewLocalStorageKey)); console.log(`returning to previously active saved view ${currentSavedViewId}`); - if(usage == "queryScreen") + if (usage == "queryScreen") { navigate(`${metaData.getTablePathByName(tableName)}/savedView/${currentSavedViewId}`); } @@ -2770,7 +2768,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init spaceAboveGrid += 60; } - if(isModal) + if (isModal) { spaceAboveGrid += 130; } @@ -2976,15 +2974,15 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init ); - if(isModal) + if (isModal) { return body; } return ( {body} - ) -}) + ); +}); RecordQuery.defaultProps = { diff --git a/src/qqq/utils/qqq/FilterUtils.tsx b/src/qqq/utils/qqq/FilterUtils.tsx index eaa8940..b2bbfd7 100644 --- a/src/qqq/utils/qqq/FilterUtils.tsx +++ b/src/qqq/utils/qqq/FilterUtils.tsx @@ -23,6 +23,7 @@ import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QControl import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {FilterVariableExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/FilterVariableExpression"; import {NowExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowExpression"; import {NowWithOffsetExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression"; import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; @@ -365,7 +366,12 @@ class FilterUtils for (let i = 0; i < maxLoops; i++) { const value = criteria.values[i]; - if (value.type == "NowWithOffset") + if (value.type == "FilterVariableExpression") + { + const expression = new FilterVariableExpression(value); + labels.push(expression.toString()); + } + else if (value.type == "NowWithOffset") { const expression = new NowWithOffsetExpression(value); labels.push(expression.toString()); @@ -657,7 +663,7 @@ class FilterUtils filterForBackend.subFilters = subFilters; - if(pageNumber !== undefined && rowsPerPage !== undefined) + if (pageNumber !== undefined && rowsPerPage !== undefined) { filterForBackend.skip = pageNumber * rowsPerPage; filterForBackend.limit = rowsPerPage; From 8dc8ae0b6d24959dcacde9142087e464f37abf21 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 30 Apr 2024 10:17:38 -0500 Subject: [PATCH 3/4] CE-1068 - Add dynamic form widget; add widgets on processes --- package.json | 2 +- src/qqq/components/forms/DynamicFormUtils.ts | 10 +- src/qqq/components/forms/EntityForm.tsx | 68 ++++- .../components/widgets/DashboardWidgets.tsx | 9 +- src/qqq/components/widgets/Widget.tsx | 3 +- .../widgets/misc/DynamicFormWidget.tsx | 264 ++++++++++++++++++ .../widgets/misc/RecordGridWidget.tsx | 92 +++--- src/qqq/pages/processes/ProcessRun.tsx | 45 +++ src/qqq/pages/records/view/RecordView.tsx | 79 +++--- src/qqq/styles/qqq-override-styles.css | 10 + 10 files changed, 495 insertions(+), 87 deletions(-) create mode 100644 src/qqq/components/widgets/misc/DynamicFormWidget.tsx diff --git a/package.json b/package.json index cea87a5..d2723e7 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.96", + "@kingsrook/qqq-frontend-core": "1.0.99", "@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 3dc736f..72f7c50 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -175,7 +175,7 @@ class DynamicFormUtils initialDisplayValue: initialDisplayValue, }; } - else + else if(processName) { dynamicFormFields[field.name].possibleValueProps = { @@ -184,6 +184,14 @@ class DynamicFormUtils initialDisplayValue: initialDisplayValue, }; } + else + { + dynamicFormFields[field.name].possibleValueProps = + { + isPossibleValue: true, + initialDisplayValue: initialDisplayValue, + }; + } } } } diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index cc7cdce..2199ff9 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -43,6 +43,7 @@ import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import MDTypography from "qqq/components/legacy/MDTypography"; import HelpContent from "qqq/components/misc/HelpContent"; import QRecordSidebar from "qqq/components/misc/RecordSidebar"; +import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget"; import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"; import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget"; @@ -409,6 +410,19 @@ function EntityForm(props: Props): JSX.Element /> } + if(widgetMetaData.type == "dynamicForm") + { + return + } + return (Unsupported widget type: {widgetMetaData.type}) } @@ -482,7 +496,7 @@ function EntityForm(props: Props): JSX.Element return (true); } - if(widget.type == "reportSetup" || widget.type == "pivotTableSetup") + if(widget.type == "reportSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm") { return (true); } @@ -706,7 +720,8 @@ function EntityForm(props: Props): JSX.Element else { 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); 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). *******************************************************************************/ @@ -981,6 +1041,10 @@ function EntityForm(props: Props): JSX.Element console.log(`Clearing value from [${fieldRule.targetField}] due to change in [${fieldName}]`); valueChangesToMake[fieldRule.targetField] = null; break; + case FieldRuleAction.RELOAD_WIDGET: + const additionalQueryParamsForWidget: {[key: string]: any} = {}; + additionalQueryParamsForWidget[fieldRule.sourceField] = newValue; + reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget); } } } diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index 20bc4fa..46132a6 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -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] && + + ) + } ); }; diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 3bfca94..f47a3c4 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -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"; diff --git a/src/qqq/components/widgets/misc/DynamicFormWidget.tsx b/src/qqq/components/widgets/misc/DynamicFormWidget.tsx new file mode 100644 index 0000000..e06e31a --- /dev/null +++ b/src/qqq/components/widgets/misc/DynamicFormWidget.tsx @@ -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 . + */ + + +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(); // 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) + { + 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 ( + + {widgetData && widgetData.noFieldsMessage} + + ); + } + + 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 ( + + + + ); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + 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 ( + {section} + ); + } + + + //////////// + // render // + //////////// + return ( + { + + {effectiveIsEditable ? renderEditForm() : renderViewForm()} + + } + ); +} + diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index b025a39..4bf329f 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -185,7 +185,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo if(data && data.viewAllLink) { labelAdditionalElementsLeft.push( - + View All ) @@ -225,8 +225,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo if(widgetMetaData?.showExportButton) { labelAdditionalElementsLeft.push( - - + + ); } @@ -305,48 +305,50 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} > - (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} - /> + + (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} + /> + ); diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index d9bfdfc..f5d1f29 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -58,6 +58,7 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; import ValidationReview from "qqq/components/processes/ValidationReview"; +import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import BaseLayout from "qqq/layouts/BaseLayout"; import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery"; 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 [showFullHelpText, setShowFullHelpText] = useState(false); + const [renderedWidgets, setRenderedWidgets] = useState({} as {[step: string]: {[widgetName: string]: any}}); + 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 (Unrecognized widget name: {widgetName}); + } + + const queryStringParts: string[] = []; + for (let name in processValues) + { + queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`) + } + + const renderedWidget = ( + + ) + renderedWidgets[activeStep.name][widgetName] = renderedWidget; + return renderedWidget; + } + //////////////////////////////////////////////////// // generate the main form body content for a step // //////////////////////////////////////////////////// @@ -653,6 +692,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is ) } + { + component.type === QComponentType.WIDGET && ( + component.values?.widgetName && + renderWidget(component.values?.widgetName) + ) + } ); })) diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index e8388a3..c29eeef 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -21,6 +21,7 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; 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 {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; 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"; + +/******************************************************************************* + ** + *******************************************************************************/ +export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: {[name: string]: QFieldMetaData} ) +{ + return + { + 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 = ; + + const labelElement = {label}:; + + return ( + + <> + { + showHelp && formattedHelpContent ? {labelElement} : labelElement + } +
 
+ + {ValueUtils.getDisplayValue(field, record, "view", fieldName)} + + +
+ ); + } + }) + } +
; +} + function RecordView({table, launchProcess}: Props): JSX.Element { 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 the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. // //////////////////////////////////////////////////////////////////////////////////////////////////////////// - const fields = ( - - { - 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 = ; - - const labelElement = {label}:; - - return ( - - <> - { - showHelp && formattedHelpContent ? {labelElement} : labelElement - } -
 
- - {ValueUtils.getDisplayValue(field, record, "view", fieldName)} - - -
- ); - } - }) - } -
- ); + const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record); if (section.tier === "T1") { @@ -979,7 +988,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element { showEditChildForm && - closeEditChildForm(event, reason)}> + closeEditChildForm(event, reason)}>
Date: Tue, 30 Apr 2024 10:18:13 -0500 Subject: [PATCH 4/4] CE-1068 - add RELOAD_WIDGET action, and targetWidget to FieldRules --- .../MaterialDashboardTableMetaData.java | 35 ++++++++++++++----- .../model/metadata/fieldrules/FieldRule.java | 33 +++++++++++++++++ .../metadata/fieldrules/FieldRuleAction.java | 3 +- src/qqq/models/fields/FieldRules.ts | 4 ++- .../MaterialDashboardTableMetaDataTest.java | 34 ++++++++++++++++++ 5 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaData.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaData.java index 96ff230..98d0bbf 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaData.java +++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaData.java @@ -134,17 +134,36 @@ public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData for(FieldRule fieldRule : CollectionUtils.nonNullList(fieldRules)) { - qInstanceValidator.assertCondition(fieldRule.getTrigger() != null, prefix + "has a fieldRule without a trigger"); - qInstanceValidator.assertCondition(fieldRule.getAction() != null, prefix + "has a fieldRule without an action"); + validateFieldRule(qInstance, tableMetaData, qInstanceValidator, fieldRule, prefix); + } + } - if(qInstanceValidator.assertCondition(StringUtils.hasContent(fieldRule.getSourceField()), prefix + "has a fieldRule without a sourceField")) - { - qInstanceValidator.assertNoException(() -> tableMetaData.getField(fieldRule.getSourceField()), prefix + "has a fieldRule with an unrecognized sourceField: " + fieldRule.getSourceField()); - } - if(qInstanceValidator.assertCondition(StringUtils.hasContent(fieldRule.getTargetField()), prefix + "has a fieldRule without a targetField")) + + /******************************************************************************* + ** + *******************************************************************************/ + static void validateFieldRule(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator, FieldRule fieldRule, String prefix) + { + qInstanceValidator.assertCondition(fieldRule.getTrigger() != null, prefix + "has a fieldRule without a trigger"); + qInstanceValidator.assertCondition(fieldRule.getAction() != null, prefix + "has a fieldRule without an action"); + + if(qInstanceValidator.assertCondition(StringUtils.hasContent(fieldRule.getSourceField()), prefix + "has a fieldRule without a sourceField")) + { + qInstanceValidator.assertNoException(() -> tableMetaData.getField(fieldRule.getSourceField()), prefix + "has a fieldRule with an unrecognized sourceField: " + fieldRule.getSourceField()); + } + + if(StringUtils.hasContent(fieldRule.getTargetField())) + { + qInstanceValidator.assertNoException(() -> tableMetaData.getField(fieldRule.getTargetField()), prefix + "has a fieldRule with an unrecognized targetField: " + fieldRule.getTargetField()); + } + + if(StringUtils.hasContent(fieldRule.getTargetWidget())) + { + if(qInstanceValidator.assertCondition(qInstance.getWidget(fieldRule.getTargetWidget()) != null, prefix + "has a widgetRule with an unrecognized targetWidget: " + fieldRule.getTargetWidget())) { - qInstanceValidator.assertNoException(() -> tableMetaData.getField(fieldRule.getTargetField()), prefix + "has a fieldRule with an unrecognized targetField: " + fieldRule.getTargetField()); + qInstanceValidator.assertCondition(CollectionUtils.nonNullList(tableMetaData.getSections()).stream().anyMatch(s -> fieldRule.getTargetWidget().equals(s.getWidgetName())), + prefix + "has a widgetRule with a targetWidget which is not used in any sections on the table"); } } } diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRule.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRule.java index a4722eb..dd2ca33 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRule.java +++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRule.java @@ -38,6 +38,8 @@ public class FieldRule implements Serializable private FieldRuleAction action; private String targetField; + private String targetWidget; + /******************************************************************************* @@ -162,4 +164,35 @@ public class FieldRule implements Serializable return (this); } + + + /******************************************************************************* + ** Getter for targetWidget + *******************************************************************************/ + public String getTargetWidget() + { + return (this.targetWidget); + } + + + + /******************************************************************************* + ** Setter for targetWidget + *******************************************************************************/ + public void setTargetWidget(String targetWidget) + { + this.targetWidget = targetWidget; + } + + + + /******************************************************************************* + ** Fluent setter for targetWidget + *******************************************************************************/ + public FieldRule withTargetWidget(String targetWidget) + { + this.targetWidget = targetWidget; + return (this); + } + } diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRuleAction.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRuleAction.java index cc112c0..8d6419b 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRuleAction.java +++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/fieldrules/FieldRuleAction.java @@ -27,5 +27,6 @@ package com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules; *******************************************************************************/ public enum FieldRuleAction { - CLEAR_TARGET_FIELD + CLEAR_TARGET_FIELD, + RELOAD_WIDGET } diff --git a/src/qqq/models/fields/FieldRules.ts b/src/qqq/models/fields/FieldRules.ts index 9812bdb..9386401 100644 --- a/src/qqq/models/fields/FieldRules.ts +++ b/src/qqq/models/fields/FieldRules.ts @@ -29,6 +29,7 @@ export interface FieldRule sourceField: string; action: FieldRuleAction; targetField: string; + targetWidget: string; } @@ -46,5 +47,6 @@ export enum FieldRuleTrigger *******************************************************************************/ export enum FieldRuleAction { - CLEAR_TARGET_FIELD = "CLEAR_TARGET_FIELD" + CLEAR_TARGET_FIELD = "CLEAR_TARGET_FIELD", + RELOAD_WIDGET = "RELOAD_WIDGET" } diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaDataTest.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaDataTest.java index 63c7521..4822e1f 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaDataTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardTableMetaDataTest.java @@ -29,6 +29,9 @@ import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.frontend.materialdashboard.junit.BaseTest; import com.kingsrook.qqq.frontend.materialdashboard.junit.TestUtils; +import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules.FieldRule; +import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules.FieldRuleAction; +import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules.FieldRuleTrigger; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -76,6 +79,37 @@ class MaterialDashboardTableMetaDataTest extends BaseTest assertValidationFailureReasons(qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).withSupplementalMetaData(new MaterialDashboardTableMetaData().withDefaultQuickFilterFieldNames(List.of("firstName", "lastName", "firstName"))), "duplicated field name: firstName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidateFieldRules() + { + assertValidationFailureReasons(qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).withSupplementalMetaData(new MaterialDashboardTableMetaData().withFieldRule(new FieldRule())), + "without an action", + "without a trigger", + "without a sourceField"); + + assertValidationFailureReasons(qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).withSupplementalMetaData(new MaterialDashboardTableMetaData().withFieldRule(new FieldRule() + .withTrigger(FieldRuleTrigger.ON_CHANGE) + .withAction(FieldRuleAction.CLEAR_TARGET_FIELD) + .withSourceField("notAField") + .withTargetField("alsoNotAField") + )), + "unrecognized sourceField: notAField", + "unrecognized targetField: alsoNotAField"); + + assertValidationFailureReasons(qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).withSupplementalMetaData(new MaterialDashboardTableMetaData().withFieldRule(new FieldRule() + .withTrigger(FieldRuleTrigger.ON_CHANGE) + .withAction(FieldRuleAction.RELOAD_WIDGET) + .withSourceField("id") + .withTargetWidget("notAWidget") + )), + "unrecognized targetWidget: notAWidget"); }