From 8074e0a61d3bca159064e5ebda56b16f01267881 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 5 May 2023 20:24:08 -0500 Subject: [PATCH 01/11] Get labels in breadcrumb from meta-data labels, not path components --- src/App.tsx | 16 ++++++++++++++-- src/QContext.tsx | 6 +++++- src/qqq/components/horseshoe/Breadcrumbs.tsx | 18 ++++++++++++++---- src/qqq/components/horseshoe/NavBar.tsx | 17 +++++++++++++++-- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 98321cd..28fb06c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,7 @@ import CssBaseline from "@mui/material/CssBaseline"; import Icon from "@mui/material/Icon"; import {ThemeProvider} from "@mui/material/styles"; import {LicenseInfo} from "@mui/x-license-pro"; -import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react"; +import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react"; import {useCookies} from "react-cookie"; import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Md5} from "ts-md5/dist/md5"; @@ -146,6 +146,7 @@ export default function App() const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true); const [sideNavRoutes, setSideNavRoutes] = useState([]); const [appRoutes, setAppRoutes] = useState(null as any); + const [pathToLabelMap, setPathToLabelMap] = useState({} as {[path: string]: string}); //////////////////////////////////////////// // load qqq meta data to make more routes // @@ -456,6 +457,14 @@ export default function App() }); } + const pathToLabelMap: {[path: string]: string} = {} + for(let i =0; i setPageHeader(header), - setAccentColor: (accentColor: string) => setAccentColor(accentColor) + setAccentColor: (accentColor: string) => setAccentColor(accentColor), + pathToLabelMap: pathToLabelMap, + branding: branding }}> @@ -568,6 +579,7 @@ export default function App() logo={branding.logo} appName={branding.appName} routes={sideNavRoutes} + pathToLabelMap={pathToLabelMap} onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave} /> diff --git a/src/QContext.tsx b/src/QContext.tsx index aaf83b2..a4dbdbf 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -19,6 +19,7 @@ * along with this program. If not, see . */ +import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData"; import {createContext} from "react"; @@ -28,11 +29,14 @@ interface QContext setPageHeader?: (header: string | JSX.Element) => void; accentColor: string; setAccentColor?: (header: string) => void; + pathToLabelMap?: {[path: string]: string}; + branding?: QBrandingMetaData; } const defaultState = { pageHeader: "", - accentColor: "#0062FF" + accentColor: "#0062FF", + pathToLabelMap: {}, }; const QContext = createContext(defaultState); diff --git a/src/qqq/components/horseshoe/Breadcrumbs.tsx b/src/qqq/components/horseshoe/Breadcrumbs.tsx index 16afa88..b4fc4ec 100644 --- a/src/qqq/components/horseshoe/Breadcrumbs.tsx +++ b/src/qqq/components/horseshoe/Breadcrumbs.tsx @@ -60,9 +60,19 @@ export const routeToLabel = (route: string): string => function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element { const routes: string[] | any = route.slice(0, -1); - const {pageHeader, setPageHeader} = useContext(QContext); + const {pageHeader, pathToLabelMap, branding} = useContext(QContext); - let pageTitle = "ColdTrack Live"; + const fullPathToLabel = (fullPath: string, route: string): string => + { + if(pathToLabelMap && pathToLabelMap[fullPath]) + { + return pathToLabelMap[fullPath]; + } + + return (routeToLabel(route)); + } + + let pageTitle = branding?.appName ?? ""; const fullRoutes: string[] = []; let accumulatedPath = ""; for (let i = 0; i < routes.length; i++) @@ -74,7 +84,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element accumulatedPath = `${accumulatedPath}/${routes[i]}`; fullRoutes.push(accumulatedPath); - pageTitle = `${routeToLabel(routes[i])} | ${pageTitle}`; + pageTitle = `${fullPathToLabel(accumulatedPath, routes[i])} | ${pageTitle}`; } document.title = `${ucFirst(title)} | ${pageTitle}`; @@ -110,7 +120,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element opacity={light ? 0.8 : 0.5} sx={{lineHeight: 0}} > - {routeToLabel(fullRoute.replace(/.*\//, ""))} + {fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))} ))} diff --git a/src/qqq/components/horseshoe/NavBar.tsx b/src/qqq/components/horseshoe/NavBar.tsx index 6d77082..647351a 100644 --- a/src/qqq/components/horseshoe/NavBar.tsx +++ b/src/qqq/components/horseshoe/NavBar.tsx @@ -30,8 +30,9 @@ import ListItemIcon from "@mui/material/ListItemIcon"; import Menu from "@mui/material/Menu"; import TextField from "@mui/material/TextField"; import Toolbar from "@mui/material/Toolbar"; -import React, {useEffect, useState} from "react"; +import React, {useContext, useEffect, useState} from "react"; import {useLocation, useNavigate} from "react-router-dom"; +import QContext from "QContext"; import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs"; import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles"; import {setTransparentNavbar, useMaterialUIController,} from "qqq/context"; @@ -60,6 +61,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element const [openMenu, setOpenMenu] = useState(false); const [history, setHistory] = useState([] as HistoryEntry[]); const [autocompleteValue, setAutocompleteValue] = useState(null); + const fullPath = useLocation().pathname; const route = useLocation().pathname.split("/").slice(1); const navigate = useNavigate(); @@ -206,7 +208,18 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element }, }); - const breadcrumbTitle = routeToLabel(route[route.length - 1]); + const {pathToLabelMap} = useContext(QContext); + const fullPathToLabel = (fullPath: string, route: string): string => + { + if(pathToLabelMap && pathToLabelMap[fullPath]) + { + return pathToLabelMap[fullPath]; + } + + return (routeToLabel(route)); + } + + const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]); return ( Date: Thu, 6 Jul 2023 18:56:20 -0500 Subject: [PATCH 02/11] Initial qfmd work to support datetime query expressions from frontend --- .../query/AdvancedDateTimeFilterValues.tsx | 207 ++++++++++++++++++ .../components/query/FilterCriteriaRow.tsx | 32 ++- .../query/FilterCriteriaRowValues.tsx | 165 +++++++++++++- src/qqq/styles/qqq-override-styles.css | 28 ++- src/qqq/utils/qqq/FilterUtils.ts | 66 +++++- 5 files changed, 476 insertions(+), 22 deletions(-) create mode 100644 src/qqq/components/query/AdvancedDateTimeFilterValues.tsx diff --git a/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx new file mode 100644 index 0000000..fe08c19 --- /dev/null +++ b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx @@ -0,0 +1,207 @@ +/* + * 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 {NowWithOffsetExpression, NowWithOffsetOperator, NowWithOffsetUnit} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression"; +import {FormControl, FormControlLabel, Radio, RadioGroup, Select} from "@mui/material"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import Grid from "@mui/material/Grid"; +import Icon from "@mui/material/Icon"; +import MenuItem from "@mui/material/MenuItem"; +import Modal from "@mui/material/Modal"; +import {SelectChangeEvent} from "@mui/material/Select/Select"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import React, {ReactNode, useState} from "react"; +import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; + + +interface Props +{ + type: "date" | "datetime"; + expression: any; + onSave: (expression: any) => void; +} + +AdvancedDateTimeFilterValues.defaultProps = {}; + +function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.Element +{ + const [originalExpression, setOriginalExpression] = useState(JSON.stringify(expression)); + + const [expressionType, setExpressionType] = useState(expression?.type ?? "NowWithOffset") + + const [amount, setAmount] = useState(expression?.amount ?? 1) + const [timeUnit, setTimeUnit] = useState(expression?.timeUnit ?? "DAYS" as NowWithOffsetUnit); + const [operator, setOperator] = useState(expression?.operator ?? "MINUS" as NowWithOffsetOperator); + const [isOpen, setIsOpen] = useState(false) + + ////////////////////////////////////////////////////////////////////////////////// + // if the expression (prop) has changed, re-set the state variables based on it // + ////////////////////////////////////////////////////////////////////////////////// + if(JSON.stringify(expression) !== originalExpression) + { + setExpressionType(expression?.type ?? "NowWithOffset") + setAmount(expression?.amount ?? 1) + setTimeUnit(expression?.timeUnit ?? "DAYS") + setOperator(expression?.operator ?? "MINUS") + setOriginalExpression(JSON.stringify(expression)) + } + + const openDialog = () => + { + setIsOpen(true); + } + + const handleSaveClicked = () => + { + switch(expressionType) + { + case "NowWithOffset": + { + const expression = new NowWithOffsetExpression() + expression.operator = operator; + expression.amount = amount; + expression.timeUnit = timeUnit; + onSave(expression); + } + } + + close(); + } + + const close = () => + { + setIsOpen(false); + } + + function handleExpressionTypeChange(e: React.ChangeEvent) + { + setExpressionType(e.target.value); + } + + function handleAmountChange(event: React.ChangeEvent) + { + setAmount(parseInt(event.target.value)); + } + + function handleTimeUnitChange(event: SelectChangeEvent, child: ReactNode) + { + // @ts-ignore + setTimeUnit(event.target.value) + } + + function handleOperatorChange(event: SelectChangeEvent, child: ReactNode) + { + // @ts-ignore + setOperator(event.target.value) + } + + + const mainCardStyles: any = {}; + mainCardStyles.width = "600px"; + + ///////////////////////////////////////////////////////////////////////// + // for the time units, have them end in an 's' if the amount is plural // + ///////////////////////////////////////////////////////////////////////// + const tuS = (amount == 1 ? "" : "s"); + + return ( + + + settings + + { + isOpen && + ( + + + + + + + + Advanced Date Filter Condition + + Select the type of expression you want for your condition.
+ Then enter values to express your condition. +
+
+
+
+ + + + } label="Relative Expression" /> + + + handleAmountChange(event)} + fullWidth + /> + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + ) + } +
+ ); +} + +export default AdvancedDateTimeFilterValues; diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index 850a065..ff6dd2d 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -159,7 +159,9 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE}); operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE}); operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE}); operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE}); operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); @@ -171,9 +173,9 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE_TIME}); operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME}); - operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is at or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME}); - operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is at or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); @@ -335,8 +337,24 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, ////////////////////////////////////////////////// // event handler for value field (of all types) // ////////////////////////////////////////////////// - const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) => + const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any, newExpression?: any) => { + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // if an expression was passed in - put it on the criteria, removing the values. // + // else - if no expression - make sure criteria.expression is null, and do the various values logics // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + if(newExpression) + { + criteria.expression = newExpression; + criteria.values = null; + updateCriteria(criteria, true); + return; + } + else + { + criteria.expression = null; + } + // @ts-ignore const value = newValue !== undefined ? newValue : event ? event.target.value : null; @@ -447,6 +465,12 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, // don't need to look at values // ////////////////////////////////// } + else if (criteria.expression) + { + //////////////////////////////////////////////////////// + // if there's an expression - let's assume it's valid // + //////////////////////////////////////////////////////// + } else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE) { if(criteria.values.length < 2) @@ -533,7 +557,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, criteria={{id: id, ...criteria}} field={field} table={fieldTable} - valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)} + valueChangeHandler={(event, valueIndex, newValue, newExpression) => handleValueChange(event, valueIndex, newValue, newExpression)} /> diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index ef1452b..b0a70e7 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -23,14 +23,21 @@ 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 {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"; 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 Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Select, {SelectChangeEvent} from "@mui/material/Select/Select"; import TextField from "@mui/material/TextField"; -import React, {SyntheticEvent, useReducer} from "react"; +import Tooltip from "@mui/material/Tooltip"; +import React, {ReactNode, SyntheticEvent, useReducer, useState} from "react"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; +import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster"; import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; @@ -42,7 +49,7 @@ interface Props criteria: QFilterCriteriaWithId; field: QFieldMetaData; table: QTableMetaData; - valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; + valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any, newExpression?: any) => void; } FilterCriteriaRowValues.defaultProps = { @@ -50,6 +57,8 @@ FilterCriteriaRowValues.defaultProps = { function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element { + const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); + const [, forceUpdate] = useReducer((x) => x + 1, 0); if (!operatorOption) @@ -122,6 +131,35 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC />; }; + const makeDateTimeExpressionTextField = (value: string, valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => + { + valueChangeHandler(event, index, ""); + forceUpdate() + document.getElementById(`${idPrefix}${criteria.id}`).focus(); + }; + + const inputProps: any = {}; + inputProps.endAdornment = ( + + clearValue(event, valueIndex)}> + close + + + ); + + return ; + } + function saveNewPasterValues(newValues: any[]) { if (criteria.values) @@ -145,6 +183,47 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC forceUpdate(); } + const openRelativeDateTimeMenu = (event: React.MouseEvent) => + { + setRelativeDateTimeMenuAnchorElement(event.currentTarget); + }; + + const closeRelativeDateTimeMenu = () => + { + setRelativeDateTimeMenuAnchorElement(null); + }; + + const setExpressionNowWithOffset = (operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => + { + const expression = new NowWithOffsetExpression() + expression.operator = operator; + expression.amount = amount; + expression.timeUnit = timeUnit; + + saveNewDateTimeExpression(expression); + + closeRelativeDateTimeMenu(); + }; + + const setExpressionThisOrLastPeriod = (operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => + { + const expression = new ThisOrLastPeriodExpression() + expression.operator = operator; + expression.timeUnit = timeUnit; + + saveNewDateTimeExpression(expression); + + closeRelativeDateTimeMenu(); + }; + + function saveNewDateTimeExpression(expression: any) + { + criteria.expression = expression; + criteria.values = null; + valueChangeHandler(null, null, null, expression); + forceUpdate(); + } + switch (operatorOption.valueMode) { case ValueMode.NONE: @@ -152,9 +231,87 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC case ValueMode.SINGLE: return makeTextField(); case ValueMode.SINGLE_DATE: - return makeTextField(); + return + { + criteria.expression == null && makeTextField() + } + { + criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression.toString()) + } + + + event_upcoming + + + setExpressionNowWithOffset("MINUS", 1, "DAYS")}>1 day ago + setExpressionThisOrLastPeriod("THIS", "DAYS")}>today + setExpressionThisOrLastPeriod("LAST", "DAYS")}>yesterday + setExpressionNowWithOffset("MINUS", 7, "DAYS")}>7 days ago + setExpressionThisOrLastPeriod("THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod("LAST", "WEEKS")}>start of last week + setExpressionNowWithOffset("MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset("MINUS", 30, "DAYS")}>30 days ago + setExpressionThisOrLastPeriod("THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod("LAST", "MONTHS")}>start of last month + setExpressionNowWithOffset("MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset("MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset("MINUS", 1, "YEARS")}>1 year ago + setExpressionThisOrLastPeriod("THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod("LAST", "YEARS")}>start of last year + + + + saveNewDateTimeExpression(expression)} /> + + ; case ValueMode.SINGLE_DATE_TIME: - return makeTextField(); + return + { + criteria.expression == null && makeTextField() + } + { + criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression.toString()) + } + + + event_upcoming + + + setExpressionNowWithOffset("MINUS", 1, "HOURS")}>1 hour ago + setExpressionThisOrLastPeriod("THIS", "HOURS")}>start of this hour + setExpressionThisOrLastPeriod("LAST", "HOURS")}>start of last hour + setExpressionNowWithOffset("MINUS", 12, "HOURS")}>12 hours ago + setExpressionNowWithOffset("MINUS", 24, "HOURS")}>24 hours ago + setExpressionThisOrLastPeriod("THIS", "DAYS")}>start of today + setExpressionThisOrLastPeriod("LAST", "DAYS")}>start of yesterday + setExpressionNowWithOffset("MINUS", 7, "DAYS")}>7 days ago + setExpressionThisOrLastPeriod("THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod("LAST", "WEEKS")}>start of last week + setExpressionNowWithOffset("MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset("MINUS", 30, "DAYS")}>30 days ago + setExpressionThisOrLastPeriod("THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod("LAST", "MONTHS")}>start of last month + setExpressionNowWithOffset("MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset("MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset("MINUS", 1, "YEARS")}>1 year ago + setExpressionThisOrLastPeriod("THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod("LAST", "YEARS")}>start of last year + + + + saveNewDateTimeExpression(expression)} /> + + ; case ValueMode.DOUBLE: return diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index 0b791d4..73d6aa5 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -446,22 +446,34 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } font-size: 14px !important; } -/* fix something in AND/OR dropdown in filters */ -.customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root +/* make down-arrow appear in select boxes */ +.customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root, +.AdvancedDateTimeFilterValues .MuiSvgIcon-root { display: inline-block !important; } -/* adjust bottom of AND/OR dropdown in filters */ -.customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl +/* adjust vertical padding in filter selects */ +.customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl .MuiSelect-select, +.AdvancedDateTimeFilterValues .MuiInputBase-formControl .MuiSelect-select { - padding-bottom: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px) !important; + padding-top: calc(0.25rem + 1px) !important; } -/* adjust down-arrow in AND/OR dropdown in filters */ -.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard +.customFilterPanel .filterValuesColumn .MuiInputBase-inputAdornedEnd { - top: calc(50% - 0.75rem); + padding-right: 0 !important; +} + +.customFilterPanel .filterValuesColumn .MuiInputAdornment-positionEnd button +{ + padding-left: 0; +} + +.customFilterPanel .filterValuesColumn .MuiSelect-iconStandard +{ + display: inline; } /* change tags in any-of value fields to not be black bg with white text */ diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index a428c2c..2c5b0dd 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -23,10 +23,12 @@ 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 {NowWithOffsetExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression"; import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression"; import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; @@ -285,6 +287,11 @@ class FilterUtils return (param); } + if (FilterUtils.gridCriteriaValueToExpression(param)) + { + return (null); + } + let rs = []; for (let i = 0; i < param.length; i++) { @@ -330,8 +337,13 @@ class FilterUtils ** Convert a filter field's value from the style that qqq uses, to the style that ** the grid uses. *******************************************************************************/ - public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], field: QFieldMetaData): any | any[] => + public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], expression: any, field: QFieldMetaData): any | any[] => { + if(expression) + { + return (expression); + } + const fieldType = field.type; if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) { @@ -342,7 +354,7 @@ class FilterUtils return (values); } - if (values.length > 0) + if (values && values.length > 0) { //////////////////////////////////////////////////////////////////////////////////////////////// // make sure dates are formatted for the grid the way it expects - not the way we pass it in. // @@ -353,7 +365,7 @@ class FilterUtils } } - return (values[0]); + return (values ? values[0] : ""); }; @@ -432,6 +444,7 @@ class FilterUtils } } + // todo - use expressions here!! if (field && field.type == "DATE_TIME" && !values) { try @@ -538,7 +551,7 @@ class FilterUtils defaultFilter.items.push({ columnField: criteria.fieldName, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values), - value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field), + value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, criteria.expression, field), id: id++, // not sure what this id is!! }); } @@ -595,6 +608,18 @@ class FilterUtils } } + if(defaultFilter && defaultFilter.items && defaultFilter.items.length) + { + defaultFilter.items.forEach((item) => + { + const expression = this.gridCriteriaValueToExpression(item.value) + if(expression) + { + item.value = expression; + } + }); + } + return ({filter: defaultFilter, sort: defaultSort}); } @@ -612,7 +637,7 @@ class FilterUtils const [field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName); if (field) { - gridItems.push({columnField: criteria.fieldName, id: i, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, criteria.values), value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, field)}); + gridItems.push({columnField: criteria.fieldName, id: i, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, criteria.values), value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, criteria.expression, field)}); } } @@ -711,7 +736,13 @@ class FilterUtils const fieldMetadata = tableMetaData?.fields.get(item.columnField); const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue); const values = FilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue, fieldMetadata); - qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values)); + let criteria = new QFilterCriteria(item.columnField, operator, values); + const expression = FilterUtils.gridCriteriaValueToExpression(item.value); + if(expression) + { + criteria.expression = expression; + } + qFilter.addCriteria(criteria); foundFilter = true; }); @@ -729,6 +760,29 @@ class FilterUtils }; + /******************************************************************************* + ** + *******************************************************************************/ + private static gridCriteriaValueToExpression(value: any) + { + if (value.length) + { + value = value[0]; + } + + if (value.type && value.type == "NowWithOffset") + { + return (new NowWithOffsetExpression(value)); + } + else if (value.type && value.type == "ThisOrLastPeriod") + { + return (new ThisOrLastPeriodExpression(value)); + } + + return (null); + } + + /******************************************************************************* ** edit the input filter object, replacing any values which have {id,label} attributes ** to instead just have the id part. From 8458ff6b2ae91482bb4a84a62e0a5dade267cb89 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 13 Jul 2023 09:18:45 -0500 Subject: [PATCH 03/11] Checkpoint - starting to add between on dates & date-times --- .../components/query/FilterCriteriaRow.tsx | 6 + .../query/FilterCriteriaRowValues.tsx | 202 ++++++++++-------- 2 files changed, 119 insertions(+), 89 deletions(-) diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index ff6dd2d..1f4049c 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -45,7 +45,9 @@ export enum ValueMode DOUBLE = "DOUBLE", MULTI = "MULTI", SINGLE_DATE = "SINGLE_DATE", + DOUBLE_DATE = "DOUBLE_DATE", SINGLE_DATE_TIME = "SINGLE_DATE_TIME", + DOUBLE_DATE_TIME = "DOUBLE_DATE_TIME", PVS_SINGLE = "PVS_SINGLE", PVS_MULTI = "PVS_MULTI", } @@ -164,6 +166,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE}); operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE}); + operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE}); //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); //? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN}); //? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN}); @@ -178,6 +182,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, operatorOptions.push({label: "is at or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME}); + operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME}); //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); //? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN}); break; diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index b0a70e7..8be4182 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -131,7 +131,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC />; }; - const makeDateTimeExpressionTextField = (value: string, valueIndex: number = 0, label = "Value", idPrefix = "value-") => + const makeDateTimeExpressionTextField = (expression: any, valueIndex: number = 0, label = "Value", idPrefix = "value-") => { const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => { @@ -143,23 +143,120 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC const inputProps: any = {}; inputProps.endAdornment = ( - clearValue(event, valueIndex)}> + clearValue(event, valueIndex)}> close ); + let displayValue = expression.toString(); + if (expression?.type == "ThisOrLastPeriod") + { + if(field.type == QFieldType.DATE_TIME || (field.type == QFieldType.DATE && expression.timeUnit != "DAYS")) + { + displayValue = "start of " + displayValue; + } + } + return ; } + const makeDateField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + return + { + criteria.expression == null && makeTextField(valueIndex, label, idPrefix) + } + { + criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression, valueIndex, label, idPrefix) + } + + + date_range + + + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "DAYS")}>1 day ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>today + setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>yesterday + setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week + setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month + setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + + + + saveNewDateTimeExpression(valueIndex, expression)} /> + + ; + } + + const makeDateTimeField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + return + { + criteria.expression == null && makeTextField(valueIndex, label, idPrefix) + } + { + criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression, valueIndex, label, idPrefix) + } + + + date_range + + + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "HOURS")}>1 hour ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "HOURS")}>start of this hour + setExpressionThisOrLastPeriod(valueIndex, "LAST", "HOURS")}>start of last hour + setExpressionNowWithOffset(valueIndex, "MINUS", 12, "HOURS")}>12 hours ago + setExpressionNowWithOffset(valueIndex, "MINUS", 24, "HOURS")}>24 hours ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>start of today + setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>start of yesterday + setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week + setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month + setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + + + + saveNewDateTimeExpression(valueIndex, expression)} /> + + ; + } + function saveNewPasterValues(newValues: any[]) { if (criteria.values) @@ -193,34 +290,34 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC setRelativeDateTimeMenuAnchorElement(null); }; - const setExpressionNowWithOffset = (operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => + const setExpressionNowWithOffset = (valueIndex: number, operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => { const expression = new NowWithOffsetExpression() expression.operator = operator; expression.amount = amount; expression.timeUnit = timeUnit; - saveNewDateTimeExpression(expression); + saveNewDateTimeExpression(valueIndex, expression); closeRelativeDateTimeMenu(); }; - const setExpressionThisOrLastPeriod = (operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => + const setExpressionThisOrLastPeriod = (valueIndex: number, operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => { const expression = new ThisOrLastPeriodExpression() expression.operator = operator; expression.timeUnit = timeUnit; - saveNewDateTimeExpression(expression); + saveNewDateTimeExpression(valueIndex, expression); closeRelativeDateTimeMenu(); }; - function saveNewDateTimeExpression(expression: any) + function saveNewDateTimeExpression(valueIndex: number, expression: any) { criteria.expression = expression; criteria.values = null; - valueChangeHandler(null, null, null, expression); + valueChangeHandler(null, valueIndex, null, expression); forceUpdate(); } @@ -231,87 +328,14 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC case ValueMode.SINGLE: return makeTextField(); case ValueMode.SINGLE_DATE: - return - { - criteria.expression == null && makeTextField() - } - { - criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression.toString()) - } - - - event_upcoming - - - setExpressionNowWithOffset("MINUS", 1, "DAYS")}>1 day ago - setExpressionThisOrLastPeriod("THIS", "DAYS")}>today - setExpressionThisOrLastPeriod("LAST", "DAYS")}>yesterday - setExpressionNowWithOffset("MINUS", 7, "DAYS")}>7 days ago - setExpressionThisOrLastPeriod("THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod("LAST", "WEEKS")}>start of last week - setExpressionNowWithOffset("MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset("MINUS", 30, "DAYS")}>30 days ago - setExpressionThisOrLastPeriod("THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod("LAST", "MONTHS")}>start of last month - setExpressionNowWithOffset("MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset("MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset("MINUS", 1, "YEARS")}>1 year ago - setExpressionThisOrLastPeriod("THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod("LAST", "YEARS")}>start of last year - - - - saveNewDateTimeExpression(expression)} /> - - ; + return makeDateField(); + case ValueMode.DOUBLE_DATE: + return + {makeDateField(0, "From", "from-")} + {makeDateField(1, "To", "to-")} + case ValueMode.SINGLE_DATE_TIME: - return - { - criteria.expression == null && makeTextField() - } - { - criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression.toString()) - } - - - event_upcoming - - - setExpressionNowWithOffset("MINUS", 1, "HOURS")}>1 hour ago - setExpressionThisOrLastPeriod("THIS", "HOURS")}>start of this hour - setExpressionThisOrLastPeriod("LAST", "HOURS")}>start of last hour - setExpressionNowWithOffset("MINUS", 12, "HOURS")}>12 hours ago - setExpressionNowWithOffset("MINUS", 24, "HOURS")}>24 hours ago - setExpressionThisOrLastPeriod("THIS", "DAYS")}>start of today - setExpressionThisOrLastPeriod("LAST", "DAYS")}>start of yesterday - setExpressionNowWithOffset("MINUS", 7, "DAYS")}>7 days ago - setExpressionThisOrLastPeriod("THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod("LAST", "WEEKS")}>start of last week - setExpressionNowWithOffset("MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset("MINUS", 30, "DAYS")}>30 days ago - setExpressionThisOrLastPeriod("THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod("LAST", "MONTHS")}>start of last month - setExpressionNowWithOffset("MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset("MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset("MINUS", 1, "YEARS")}>1 year ago - setExpressionThisOrLastPeriod("THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod("LAST", "YEARS")}>start of last year - - - - saveNewDateTimeExpression(expression)} /> - - ; + return makeDateTimeField(); case ValueMode.DOUBLE: return From 28c48cc2ef5ee873642eb6bf09f6f123f5d5b460 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 14 Jul 2023 09:36:33 -0500 Subject: [PATCH 04/11] Checkpoint - functional between, expressions working as values, etc --- .../query/AdvancedDateTimeFilterValues.tsx | 161 +++++++-- .../components/query/CriteriaDateField.tsx | 221 ++++++++++++ .../components/query/FilterCriteriaRow.tsx | 30 +- .../query/FilterCriteriaRowValues.tsx | 337 +++++------------- src/qqq/utils/qqq/FilterUtils.ts | 31 +- 5 files changed, 453 insertions(+), 327 deletions(-) create mode 100644 src/qqq/components/query/CriteriaDateField.tsx diff --git a/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx index fe08c19..c94919f 100644 --- a/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx +++ b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx @@ -19,7 +19,10 @@ * along with this program. If not, see . */ +import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +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"; import {FormControl, FormControlLabel, Radio, RadioGroup, Select} from "@mui/material"; import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; @@ -37,33 +40,57 @@ import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons" interface Props { - type: "date" | "datetime"; + type: QFieldType expression: any; onSave: (expression: any) => void; } AdvancedDateTimeFilterValues.defaultProps = {}; +const extractExpressionType = (expression: any) => expression?.type ?? "NowWithOffset"; +const extractNowWithOffsetAmount = (expression: any) => expression?.type == "NowWithOffset" ? (expression?.amount ?? 1) : 1; +const extractNowWithOffsetTimeUnit = (expression: any) => expression?.type == "NowWithOffset" ? (expression?.timeUnit ?? "DAYS") : "DAYS" as NowWithOffsetUnit; +const extractNowWithOffsetOperator = (expression: any) => expression?.type == "NowWithOffset" ? (expression?.operator ?? "MINUS") : "MINUS" as NowWithOffsetOperator; +const extractThisOrLastPeriodTimeUnit = (expression: any) => expression?.type == "ThisOrLastPeriod" ? (expression?.timeUnit ?? "DAYS") : "DAYS" as ThisOrLastPeriodUnit; +const extractThisOrLastPeriodOperator = (expression: any) => expression?.type == "ThisOrLastPeriod" ? (expression?.operator ?? "THIS") : "THIS" as ThisOrLastPeriodOperator; + function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.Element { const [originalExpression, setOriginalExpression] = useState(JSON.stringify(expression)); - const [expressionType, setExpressionType] = useState(expression?.type ?? "NowWithOffset") + const [expressionType, setExpressionType] = useState(extractExpressionType(expression)) + + const [nowWithOffsetAmount, setNowWithOffsetAmount] = useState(extractNowWithOffsetAmount(expression)); + const [nowWithOffsetTimeUnit, setNowWithOffsetTimeUnit] = useState(extractNowWithOffsetTimeUnit(expression)); + const [nowWithOffsetOperator, setNowWithOffsetOperator] = useState(extractNowWithOffsetOperator(expression)); + + const [thisOrLastPeriodTimeUnit, setThisOrLastPeriodTimeUnit] = useState(extractThisOrLastPeriodTimeUnit(expression)); + const [thisOrLastPeriodOperator, setThisOrLastPeriodOperator] = useState(extractThisOrLastPeriodOperator(expression)); - const [amount, setAmount] = useState(expression?.amount ?? 1) - const [timeUnit, setTimeUnit] = useState(expression?.timeUnit ?? "DAYS" as NowWithOffsetUnit); - const [operator, setOperator] = useState(expression?.operator ?? "MINUS" as NowWithOffsetOperator); const [isOpen, setIsOpen] = useState(false) + const setStateToExpression = (activeExpression: any) => + { + setExpressionType(extractExpressionType(activeExpression)) + + setNowWithOffsetAmount(extractNowWithOffsetAmount(activeExpression)) + setNowWithOffsetTimeUnit(extractNowWithOffsetTimeUnit(activeExpression)) + setNowWithOffsetOperator(extractNowWithOffsetOperator(activeExpression)) + + setThisOrLastPeriodTimeUnit(extractThisOrLastPeriodTimeUnit(activeExpression)) + setThisOrLastPeriodOperator(extractThisOrLastPeriodOperator(activeExpression)) + } + ////////////////////////////////////////////////////////////////////////////////// // if the expression (prop) has changed, re-set the state variables based on it // ////////////////////////////////////////////////////////////////////////////////// if(JSON.stringify(expression) !== originalExpression) { - setExpressionType(expression?.type ?? "NowWithOffset") - setAmount(expression?.amount ?? 1) - setTimeUnit(expression?.timeUnit ?? "DAYS") - setOperator(expression?.operator ?? "MINUS") + /////////////////////////////////////////////////////////// + // update all state vars based on the current expression // + /////////////////////////////////////////////////////////// + setStateToExpression(expression); + setOriginalExpression(JSON.stringify(expression)) } @@ -72,17 +99,47 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El setIsOpen(true); } + const handleCancelClicked = () => + { + /////////////////////////////////////////////////////////// + // update all state vars back to the original expression // + /////////////////////////////////////////////////////////// + const restoreExpression = JSON.parse(originalExpression) + setStateToExpression(restoreExpression); + + close(); + } + const handleSaveClicked = () => { switch(expressionType) { + case "Now": + { + const expression = new NowExpression(); + onSave(expression); + break; + } case "NowWithOffset": { const expression = new NowWithOffsetExpression() - expression.operator = operator; - expression.amount = amount; - expression.timeUnit = timeUnit; + expression.operator = nowWithOffsetOperator; + expression.amount = nowWithOffsetAmount; + expression.timeUnit = nowWithOffsetTimeUnit; onSave(expression); + break; + } + case "ThisOrLastPeriod": + { + const expression = new ThisOrLastPeriodExpression() + expression.operator = thisOrLastPeriodOperator; + expression.timeUnit = thisOrLastPeriodTimeUnit; + onSave(expression); + break; + } + default: + { + console.log(`Unmapped expression type in handleSAveClicked: ${expressionType}`); } } @@ -99,35 +156,47 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El setExpressionType(e.target.value); } - function handleAmountChange(event: React.ChangeEvent) + function handleNowWithOffsetAmountChange(event: React.ChangeEvent) { - setAmount(parseInt(event.target.value)); + setNowWithOffsetAmount(parseInt(event.target.value)); } - function handleTimeUnitChange(event: SelectChangeEvent, child: ReactNode) + function handleNowWithOffsetTimeUnitChange(event: SelectChangeEvent, child: ReactNode) { // @ts-ignore - setTimeUnit(event.target.value) + setNowWithOffsetTimeUnit(event.target.value) } - function handleOperatorChange(event: SelectChangeEvent, child: ReactNode) + function handleNowWithOffsetOperatorChange(event: SelectChangeEvent, child: ReactNode) { // @ts-ignore - setOperator(event.target.value) + setNowWithOffsetOperator(event.target.value) } + function handleThisOrLastPeriodTimeUnitChange(event: SelectChangeEvent, child: ReactNode) + { + // @ts-ignore + setThisOrLastPeriodTimeUnit(event.target.value) + } + + function handleThisOrLastPeriodOperatorChange(event: SelectChangeEvent, child: ReactNode) + { + // @ts-ignore + setThisOrLastPeriodOperator(event.target.value) + } const mainCardStyles: any = {}; mainCardStyles.width = "600px"; ///////////////////////////////////////////////////////////////////////// // for the time units, have them end in an 's' if the amount is plural // + // name here means "time unit 's'" // ///////////////////////////////////////////////////////////////////////// - const tuS = (amount == 1 ? "" : "s"); + const nwoTUs = (nowWithOffsetAmount == 1 ? "" : "s"); return ( - + settings { @@ -150,6 +219,10 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El + + } label="Now" /> + + } label="Relative Expression" /> @@ -159,26 +232,26 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El type="number" inputProps={{min: 0}} autoComplete="off" - value={amount} - onChange={(event) => handleAmountChange(event)} + value={nowWithOffsetAmount} + onChange={(event) => handleNowWithOffsetAmountChange(event)} fullWidth /> - + {type == QFieldType.DATE_TIME && Second{nwoTUs}} + {type == QFieldType.DATE_TIME && Minute{nwoTUs}} + {type == QFieldType.DATE_TIME && Hour{nwoTUs}} + Day{nwoTUs} + Week{nwoTUs} + Month{nwoTUs} + Year{nwoTUs} - Ago (in the past) From now (in the future) @@ -186,10 +259,34 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El + + } label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} /> + + + + + + + + + + + + + - + diff --git a/src/qqq/components/query/CriteriaDateField.tsx b/src/qqq/components/query/CriteriaDateField.tsx new file mode 100644 index 0000000..c6b7c00 --- /dev/null +++ b/src/qqq/components/query/CriteriaDateField.tsx @@ -0,0 +1,221 @@ +/* + * 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 {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +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"; +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 Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; +import React, {SyntheticEvent, useReducer, useState} from "react"; +import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues"; +import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; +import {makeTextField} from "qqq/components/query/FilterCriteriaRowValues"; + +interface CriteriaDateFieldProps +{ + valueIndex: number; + label: string; + idPrefix: string; + field: QFieldMetaData; + criteria: QFilterCriteriaWithId; + valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; +} + +CriteriaDateField.defaultProps = { + valueIndex: 0, + label: "Value", + idPrefix: "value-" +}; + +export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler}: CriteriaDateFieldProps): JSX.Element +{ + const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); + const [, forceUpdate] = useReducer((x) => x + 1, 0); + + const openRelativeDateTimeMenu = (event: React.MouseEvent) => + { + setRelativeDateTimeMenuAnchorElement(event.currentTarget); + }; + + const closeRelativeDateTimeMenu = () => + { + setRelativeDateTimeMenuAnchorElement(null); + }; + + const setExpressionNow = (valueIndex: number) => + { + const expression = new NowExpression() + saveNewDateTimeExpression(valueIndex, expression); + + closeRelativeDateTimeMenu(); + }; + + const setExpressionNowWithOffset = (valueIndex: number, operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => + { + const expression = new NowWithOffsetExpression() + expression.operator = operator; + expression.amount = amount; + expression.timeUnit = timeUnit; + + saveNewDateTimeExpression(valueIndex, expression); + + closeRelativeDateTimeMenu(); + }; + + const setExpressionThisOrLastPeriod = (valueIndex: number, operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => + { + const expression = new ThisOrLastPeriodExpression() + expression.operator = operator; + expression.timeUnit = timeUnit; + + saveNewDateTimeExpression(valueIndex, expression); + + closeRelativeDateTimeMenu(); + }; + + function saveNewDateTimeExpression(valueIndex: number, expression: any) + { + valueChangeHandler(null, valueIndex, expression); + forceUpdate(); + } + + const makeDateTimeExpressionTextField = (expression: any, valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => + { + valueChangeHandler(event, index, ""); + forceUpdate() + document.getElementById(`${idPrefix}${criteria.id}`).focus(); + }; + + const inputProps: any = {}; + inputProps.endAdornment = ( + + clearValue(event, valueIndex)}> + close + + + ); + + let displayValue = expression.toString(); + if (expression?.type == "ThisOrLastPeriod") + { + if(field.type == QFieldType.DATE_TIME || (field.type == QFieldType.DATE && expression.timeUnit != "DAYS")) + { + displayValue = "start of " + displayValue; + } + } + + return ; + } + + const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; + const currentExpression = isExpression ? criteria.values[valueIndex] : null; + + return + { + isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) + : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix) + } + + + date_range + + + { + field.type == QFieldType.DATE ? + + + setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + + + setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>today + setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>yesterday + setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week + setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month + setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + + + : + + + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "HOURS")}>1 hour ago + setExpressionNowWithOffset(valueIndex, "MINUS", 12, "HOURS")}>12 hours ago + setExpressionNowWithOffset(valueIndex, "MINUS", 24, "HOURS")}>24 hours ago + setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago + setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + + + setExpressionNow(valueIndex)}>now + setExpressionThisOrLastPeriod(valueIndex, "THIS", "HOURS")}>start of this hour + setExpressionThisOrLastPeriod(valueIndex, "LAST", "HOURS")}>start of last hour + setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>start of today + setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>start of yesterday + setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week + setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week + setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month + setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month + setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year + setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + + + } + + + + saveNewDateTimeExpression(valueIndex, expression)} /> + + ; +} diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index 1f4049c..8114c63 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -343,24 +343,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, ////////////////////////////////////////////////// // event handler for value field (of all types) // ////////////////////////////////////////////////// - const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any, newExpression?: any) => + const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) => { - /////////////////////////////////////////////////////////////////////////////////////////////////////// - // if an expression was passed in - put it on the criteria, removing the values. // - // else - if no expression - make sure criteria.expression is null, and do the various values logics // - /////////////////////////////////////////////////////////////////////////////////////////////////////// - if(newExpression) - { - criteria.expression = newExpression; - criteria.values = null; - updateCriteria(criteria, true); - return; - } - else - { - criteria.expression = null; - } - // @ts-ignore const value = newValue !== undefined ? newValue : event ? event.target.value : null; @@ -471,15 +455,9 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, // don't need to look at values // ////////////////////////////////// } - else if (criteria.expression) + else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE_TIME) { - //////////////////////////////////////////////////////// - // if there's an expression - let's assume it's valid // - //////////////////////////////////////////////////////// - } - else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE) - { - if(criteria.values.length < 2) + if(criteria.values.length < 2 || isNotSet(criteria.values[0]) || isNotSet(criteria.values[1])) { criteriaIsValid = false; criteriaStatusTooltip = "You must enter two values to complete the definition of this condition."; @@ -563,7 +541,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, criteria={{id: id, ...criteria}} field={field} table={fieldTable} - valueChangeHandler={(event, valueIndex, newValue, newExpression) => handleValueChange(event, valueIndex, newValue, newExpression)} + valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)} /> diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index 8be4182..d1eb339 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -23,21 +23,15 @@ 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 {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"; 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 Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Select, {SelectChangeEvent} from "@mui/material/Select/Select"; import TextField from "@mui/material/TextField"; -import Tooltip from "@mui/material/Tooltip"; -import React, {ReactNode, SyntheticEvent, useReducer, useState} from "react"; +import React, {SyntheticEvent, useReducer} from "react"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; -import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues"; +import CriteriaDateField from "qqq/components/query/CriteriaDateField"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster"; import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; @@ -49,16 +43,78 @@ interface Props criteria: QFilterCriteriaWithId; field: QFieldMetaData; table: QTableMetaData; - valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any, newExpression?: any) => void; + valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; } -FilterCriteriaRowValues.defaultProps = { +FilterCriteriaRowValues.defaultProps = {}; + +export const getTypeForTextField = (field: QFieldMetaData): string => +{ + let type = "search"; + + if (field.type == QFieldType.INTEGER) + { + type = "number"; + } + else if (field.type == QFieldType.DATE) + { + type = "date"; + } + else if (field.type == QFieldType.DATE_TIME) + { + type = "datetime-local"; + } + + 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-") => +{ + let type = getTypeForTextField(field); + const inputLabelProps: any = {}; + + if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME) + { + inputLabelProps.shrink = true; + } + + let value = criteria.values[valueIndex]; + if (field.type == QFieldType.DATE_TIME && value && String(value).indexOf("Z") > -1) + { + value = ValueUtils.formatDateTimeValueForForm(value); + } + + const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => + { + valueChangeHandler(event, index, ""); + document.getElementById(`${idPrefix}${criteria.id}`).focus(); + }; + + const inputProps: any = {}; + inputProps.endAdornment = ( + + clearValue(event, valueIndex)}> + close + + + ); + + return valueChangeHandler(event, valueIndex)} + value={value} + InputLabelProps={inputLabelProps} + InputProps={inputProps} + fullWidth + />; }; function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element { - const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); - const [, forceUpdate] = useReducer((x) => x + 1, 0); if (!operatorOption) @@ -66,197 +122,6 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC return
; } - const getTypeForTextField = (): string => - { - let type = "search"; - - if (field.type == QFieldType.INTEGER) - { - type = "number"; - } - else if (field.type == QFieldType.DATE) - { - type = "date"; - } - else if (field.type == QFieldType.DATE_TIME) - { - type = "datetime-local"; - } - - return (type); - }; - - const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => - { - let type = getTypeForTextField(); - const inputLabelProps: any = {}; - - if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME) - { - inputLabelProps.shrink = true; - } - - let value = criteria.values[valueIndex]; - if (field.type == QFieldType.DATE_TIME && value && String(value).indexOf("Z") > -1) - { - value = ValueUtils.formatDateTimeValueForForm(value); - } - - const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => - { - valueChangeHandler(event, index, ""); - document.getElementById(`${idPrefix}${criteria.id}`).focus(); - }; - - const inputProps: any = {}; - inputProps.endAdornment = ( - - clearValue(event, valueIndex)}> - close - - - ); - - return valueChangeHandler(event, valueIndex)} - value={value} - InputLabelProps={inputLabelProps} - InputProps={inputProps} - fullWidth - />; - }; - - const makeDateTimeExpressionTextField = (expression: any, valueIndex: number = 0, label = "Value", idPrefix = "value-") => - { - const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => - { - valueChangeHandler(event, index, ""); - forceUpdate() - document.getElementById(`${idPrefix}${criteria.id}`).focus(); - }; - - const inputProps: any = {}; - inputProps.endAdornment = ( - - clearValue(event, valueIndex)}> - close - - - ); - - let displayValue = expression.toString(); - if (expression?.type == "ThisOrLastPeriod") - { - if(field.type == QFieldType.DATE_TIME || (field.type == QFieldType.DATE && expression.timeUnit != "DAYS")) - { - displayValue = "start of " + displayValue; - } - } - - return ; - } - - const makeDateField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => - { - return - { - criteria.expression == null && makeTextField(valueIndex, label, idPrefix) - } - { - criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression, valueIndex, label, idPrefix) - } - - - date_range - - - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "DAYS")}>1 day ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>today - setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>yesterday - setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week - setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month - setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year - - - - saveNewDateTimeExpression(valueIndex, expression)} /> - - ; - } - - const makeDateTimeField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => - { - return - { - criteria.expression == null && makeTextField(valueIndex, label, idPrefix) - } - { - criteria.expression != null && makeDateTimeExpressionTextField(criteria.expression, valueIndex, label, idPrefix) - } - - - date_range - - - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "HOURS")}>1 hour ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "HOURS")}>start of this hour - setExpressionThisOrLastPeriod(valueIndex, "LAST", "HOURS")}>start of last hour - setExpressionNowWithOffset(valueIndex, "MINUS", 12, "HOURS")}>12 hours ago - setExpressionNowWithOffset(valueIndex, "MINUS", 24, "HOURS")}>24 hours ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>start of today - setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>start of yesterday - setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week - setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month - setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago - setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year - - - - saveNewDateTimeExpression(valueIndex, expression)} /> - - ; - } - function saveNewPasterValues(newValues: any[]) { if (criteria.values) @@ -280,69 +145,33 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC forceUpdate(); } - const openRelativeDateTimeMenu = (event: React.MouseEvent) => - { - setRelativeDateTimeMenuAnchorElement(event.currentTarget); - }; - - const closeRelativeDateTimeMenu = () => - { - setRelativeDateTimeMenuAnchorElement(null); - }; - - const setExpressionNowWithOffset = (valueIndex: number, operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => - { - const expression = new NowWithOffsetExpression() - expression.operator = operator; - expression.amount = amount; - expression.timeUnit = timeUnit; - - saveNewDateTimeExpression(valueIndex, expression); - - closeRelativeDateTimeMenu(); - }; - - const setExpressionThisOrLastPeriod = (valueIndex: number, operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => - { - const expression = new ThisOrLastPeriodExpression() - expression.operator = operator; - expression.timeUnit = timeUnit; - - saveNewDateTimeExpression(valueIndex, expression); - - closeRelativeDateTimeMenu(); - }; - - function saveNewDateTimeExpression(valueIndex: number, expression: any) - { - criteria.expression = expression; - criteria.values = null; - valueChangeHandler(null, valueIndex, null, expression); - forceUpdate(); - } - switch (operatorOption.valueMode) { case ValueMode.NONE: return
; case ValueMode.SINGLE: - return makeTextField(); + return makeTextField(field, criteria, valueChangeHandler); case ValueMode.SINGLE_DATE: - return makeDateField(); + return ; case ValueMode.DOUBLE_DATE: return - {makeDateField(0, "From", "from-")} - {makeDateField(1, "To", "to-")} - + + +
; case ValueMode.SINGLE_DATE_TIME: - return makeDateTimeField(); + return ; + case ValueMode.DOUBLE_DATE_TIME: + return + + + ; case ValueMode.DOUBLE: return - { makeTextField(0, "From", "from-") } + {makeTextField(field, criteria, valueChangeHandler, 0, "From", "from-")} - {makeTextField(1, "To", "to-")} + {makeTextField(field, criteria, valueChangeHandler, 1, "To", "to-")} ; case ValueMode.MULTI: @@ -365,7 +194,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC onChange={(event, value) => valueChangeHandler(event, "all", value)} /> - saveNewPasterValues(newValues)} /> + saveNewPasterValues(newValues)} />
; case ValueMode.PVS_SINGLE: @@ -414,7 +243,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC inForm={false} onChange={(value: any) => valueChangeHandler(null, "all", value)} /> -
+
; } return (
); diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index 2c5b0dd..d3c4372 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -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 {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"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; @@ -289,7 +290,7 @@ class FilterUtils if (FilterUtils.gridCriteriaValueToExpression(param)) { - return (null); + return (param); } let rs = []; @@ -337,13 +338,8 @@ class FilterUtils ** Convert a filter field's value from the style that qqq uses, to the style that ** the grid uses. *******************************************************************************/ - public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], expression: any, field: QFieldMetaData): any | any[] => + public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], field: QFieldMetaData): any | any[] => { - if(expression) - { - return (expression); - } - const fieldType = field.type; if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) { @@ -361,7 +357,13 @@ class FilterUtils //////////////////////////////////////////////////////////////////////////////////////////////// if (fieldType === QFieldType.DATE_TIME) { - values[0] = ValueUtils.formatDateTimeValueForForm(values[0]); + for(let i = 0; i Date: Mon, 17 Jul 2023 08:48:54 -0500 Subject: [PATCH 05/11] Adding tooltips w/ evaluated real-time values; fixing bugs, getting ready for release --- .../query/AdvancedDateTimeFilterValues.tsx | 24 ++- .../components/query/CriteriaDateField.tsx | 193 +++++++++++------- .../components/query/EvaluatedExpression.tsx | 189 +++++++++++++++++ src/qqq/utils/DataGridUtils.tsx | 22 +- src/qqq/utils/qqq/FilterUtils.ts | 189 ++++++----------- src/qqq/utils/qqq/ValueUtils.tsx | 10 + 6 files changed, 426 insertions(+), 201 deletions(-) create mode 100644 src/qqq/components/query/EvaluatedExpression.tsx diff --git a/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx index c94919f..085d01c 100644 --- a/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx +++ b/src/qqq/components/query/AdvancedDateTimeFilterValues.tsx @@ -43,9 +43,12 @@ interface Props type: QFieldType expression: any; onSave: (expression: any) => void; + forcedOpen: boolean; } -AdvancedDateTimeFilterValues.defaultProps = {}; +AdvancedDateTimeFilterValues.defaultProps = { + forcedOpen: false +}; const extractExpressionType = (expression: any) => expression?.type ?? "NowWithOffset"; const extractNowWithOffsetAmount = (expression: any) => expression?.type == "NowWithOffset" ? (expression?.amount ?? 1) : 1; @@ -54,7 +57,7 @@ const extractNowWithOffsetOperator = (expression: any) => expression?.type == "N const extractThisOrLastPeriodTimeUnit = (expression: any) => expression?.type == "ThisOrLastPeriod" ? (expression?.timeUnit ?? "DAYS") : "DAYS" as ThisOrLastPeriodUnit; const extractThisOrLastPeriodOperator = (expression: any) => expression?.type == "ThisOrLastPeriod" ? (expression?.operator ?? "THIS") : "THIS" as ThisOrLastPeriodOperator; -function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.Element +function AdvancedDateTimeFilterValues({type, expression, onSave, forcedOpen}: Props): JSX.Element { const [originalExpression, setOriginalExpression] = useState(JSON.stringify(expression)); @@ -69,6 +72,11 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El const [isOpen, setIsOpen] = useState(false) + if(!isOpen && forcedOpen) + { + setIsOpen(true); + } + const setStateToExpression = (activeExpression: any) => { setExpressionType(extractExpressionType(activeExpression)) @@ -196,8 +204,8 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El return ( - - settings + + settings { isOpen && @@ -209,7 +217,7 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El - Advanced Date Filter Condition + Custom Date Filter Condition Select the type of expression you want for your condition.
Then enter values to express your condition. @@ -220,11 +228,11 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El - } label="Now" /> + } label={type == QFieldType.DATE_TIME ? "Now" : "Today"} /> - } label="Relative Expression" /> + } label="Relative Expression" /> - } label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} /> + } label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} /> diff --git a/src/qqq/components/query/CriteriaDateField.tsx b/src/qqq/components/query/CriteriaDateField.tsx index c6b7c00..7040c5d 100644 --- a/src/qqq/components/query/CriteriaDateField.tsx +++ b/src/qqq/components/query/CriteriaDateField.tsx @@ -25,18 +25,25 @@ import {NowExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowExp 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"; import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; import Icon from "@mui/material/Icon"; import IconButton from "@mui/material/IconButton"; import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import {styled} from "@mui/material/styles"; import TextField from "@mui/material/TextField"; -import Tooltip from "@mui/material/Tooltip"; -import React, {SyntheticEvent, useReducer, useState} from "react"; +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"; + +export type Expression = NowWithOffsetExpression | ThisOrLastPeriodExpression | NowExpression; + + interface CriteriaDateFieldProps { valueIndex: number; @@ -56,6 +63,7 @@ CriteriaDateField.defaultProps = { export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler}: CriteriaDateFieldProps): JSX.Element { const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); + const [forceAdvancedDateTimeDialogOpen, setForceAdvancedDateTimeDialogOpen] = useState(false) const [, forceUpdate] = useReducer((x) => x + 1, 0); const openRelativeDateTimeMenu = (event: React.MouseEvent) => @@ -68,34 +76,9 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c setRelativeDateTimeMenuAnchorElement(null); }; - const setExpressionNow = (valueIndex: number) => + const setExpression = (valueIndex: number, expression: Expression) => { - const expression = new NowExpression() saveNewDateTimeExpression(valueIndex, expression); - - closeRelativeDateTimeMenu(); - }; - - const setExpressionNowWithOffset = (valueIndex: number, operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit) => - { - const expression = new NowWithOffsetExpression() - expression.operator = operator; - expression.amount = amount; - expression.timeUnit = timeUnit; - - saveNewDateTimeExpression(valueIndex, expression); - - closeRelativeDateTimeMenu(); - }; - - const setExpressionThisOrLastPeriod = (valueIndex: number, operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit) => - { - const expression = new ThisOrLastPeriodExpression() - expression.operator = operator; - expression.timeUnit = timeUnit; - - saveNewDateTimeExpression(valueIndex, expression); - closeRelativeDateTimeMenu(); }; @@ -110,7 +93,7 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => { valueChangeHandler(event, index, ""); - forceUpdate() + forceUpdate(); document.getElementById(`${idPrefix}${criteria.id}`).focus(); }; @@ -126,13 +109,20 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c let displayValue = expression.toString(); if (expression?.type == "ThisOrLastPeriod") { - if(field.type == QFieldType.DATE_TIME || (field.type == QFieldType.DATE && expression.timeUnit != "DAYS")) + if (field.type == QFieldType.DATE_TIME || (field.type == QFieldType.DATE && expression.timeUnit != "DAYS")) { displayValue = "start of " + displayValue; } } + if (expression?.type == "Now") + { + if (field.type == QFieldType.DATE) + { + displayValue = "today"; + } + } - return } placement="bottom" enterDelay={1000} sx={{marginLeft: "-75px !important", marginTop: "-8px !important"}}>; - } + />; + }; 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(field.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS") + { + startOfPrefix = "start of "; + } + } + + return } placement={tooltipPlacement}> + setExpression(valueIndex, expression)}>{startOfPrefix}{expression.toString()} + ; + }; + + const newNowExpression = (): NowExpression => + { + const expression = new NowExpression(); + return (expression); + }; + + const newNowWithOffsetExpression = (operator: NowWithOffsetOperator, amount: number, timeUnit: NowWithOffsetUnit): NowWithOffsetExpression => + { + const expression = new NowWithOffsetExpression(); + expression.operator = operator; + expression.amount = amount; + expression.timeUnit = timeUnit; + return (expression); + }; + + const newThisOrLastPeriodExpression = (operator: ThisOrLastPeriodOperator, timeUnit: ThisOrLastPeriodUnit): ThisOrLastPeriodExpression => + { + const expression = new ThisOrLastPeriodExpression(); + expression.operator = operator; + expression.timeUnit = timeUnit; + return (expression); + }; + + function doForceAdvancedDateTimeDialogOpen() + { + setForceAdvancedDateTimeDialogOpen(true); + closeRelativeDateTimeMenu(); + setTimeout(() => setForceAdvancedDateTimeDialogOpen(false), 100); + } + return { isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix) } - - date_range + + date_range - setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))} + + + Custom + - setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>today - setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>yesterday - setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week - setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month - setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))} : - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "HOURS")}>1 hour ago - setExpressionNowWithOffset(valueIndex, "MINUS", 12, "HOURS")}>12 hours ago - setExpressionNowWithOffset(valueIndex, "MINUS", 24, "HOURS")}>24 hours ago - setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago - setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "HOURS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 12, "HOURS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 24, "HOURS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))} + + + Custom + - setExpressionNow(valueIndex)}>now - setExpressionThisOrLastPeriod(valueIndex, "THIS", "HOURS")}>start of this hour - setExpressionThisOrLastPeriod(valueIndex, "LAST", "HOURS")}>start of last hour - setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>start of today - setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>start of yesterday - setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week - setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week - setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month - setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month - setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year - setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year + {tooltipMenuItemFromExpression(valueIndex, "right", newNowExpression())} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "HOURS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "HOURS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))} + {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))} } - saveNewDateTimeExpression(valueIndex, expression)} /> + saveNewDateTimeExpression(valueIndex, expression)} forcedOpen={forceAdvancedDateTimeDialogOpen} /> ; } + diff --git a/src/qqq/components/query/EvaluatedExpression.tsx b/src/qqq/components/query/EvaluatedExpression.tsx new file mode 100644 index 0000000..9ab4f53 --- /dev/null +++ b/src/qqq/components/query/EvaluatedExpression.tsx @@ -0,0 +1,189 @@ +/* + * 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 {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"; + +/******************************************************************************* + ** Helper component to show value inside tooltips that ticks up every second. + ** Without this, changing state on the higher-level component caused the tooltip to flicker. + *******************************************************************************/ +interface EvaluatedExpressionProps +{ + field: QFieldMetaData; + expression: any; +} + + +export function EvaluatedExpression({field, expression}: EvaluatedExpressionProps) +{ + const [timeForEvaluations, setTimeForEvaluations] = useState(new Date()); + + useEffect(() => + { + const interval = setInterval(() => + { + setTimeForEvaluations(new Date()); + }, 1000); + + return () => clearInterval(interval); + }, []); + + return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}; +} + +const HOUR_MS = 60 * 60 * 1000; +const DAY_MS = 24 * 60 * 60 * 1000; +const evaluateExpression = (time: Date, field: QFieldMetaData, expression: Expression): string => +{ + let rs: Date = null; + if (expression.type == "NowWithOffset") + { + rs = time; + let amount = Number(expression.amount); + switch (expression.timeUnit) + { + case "MINUTES": + { + amount = amount * 60 * 1000; + break; + } + case "HOURS": + { + amount = amount * HOUR_MS; + break; + } + case "DAYS": + { + amount = amount * DAY_MS; + break; + } + case "YEARS": + { + amount = amount * 365 * DAY_MS; + break; + } + default: + { + console.log("Unrecognized time unit: " + expression.timeUnit); + } + } + + if (expression.operator == "MINUS") + { + amount = -amount; + } + + rs.setTime(rs.getTime() + amount); + + if (expression.timeUnit == "YEARS") + { + ////////////////////// + // handle leap year // + ////////////////////// + const today = time; + while (today.getDate() != rs.getDate()) + { + rs.setTime(rs.getTime() - DAY_MS); + } + } + } + else if (expression.type == "Now") + { + rs = time; + } + else if (expression.type == "ThisOrLastPeriod") + { + rs = time; + rs.setSeconds(0); + rs.setMinutes(0); + if (expression.timeUnit == "HOURS") + { + if (expression.operator == "LAST") + { + rs.setTime(rs.getTime() - HOUR_MS); + } + } + else + { + rs.setHours(0); + if (expression.timeUnit == "DAYS") + { + if (expression.operator == "LAST") + { + rs.setTime(rs.getTime() - DAY_MS); + } + } + else if (expression.timeUnit == "WEEKS") + { + while (rs.getDay() != 0) + { + rs.setTime(rs.getTime() - DAY_MS); + } + + if (expression.operator == "LAST") + { + rs.setTime(rs.getTime() - 7 * DAY_MS); + } + } + else if (expression.timeUnit == "MONTHS") + { + rs.setDate(1); + + if (expression.operator == "LAST") + { + rs.setTime(rs.getTime() - DAY_MS); + rs.setDate(1); + } + } + else if (expression.timeUnit == "YEARS") + { + rs.setDate(1); + rs.setMonth(0); + + if (expression.operator == "LAST") + { + rs.setTime(rs.getTime() - 365 * DAY_MS); + } + } + } + } + + if (rs) + { + if (field.type == QFieldType.DATE) + { + return (ValueUtils.formatDate(rs)); + } + else + { + return (ValueUtils.formatDateTime(rs)); + } + } + + return null; +}; + + + diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 6a04389..0aa5626 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -50,15 +50,33 @@ const makeGridFilterOperator = (value: string, label: string, takesValues: boole return (rs); }; +//////////////////////////////////////////////////////////////////////////////////////// +// at this point, these may only be used to drive the toolitp on the FILTER button... // +//////////////////////////////////////////////////////////////////////////////////////// const QGridDateOperators = [ makeGridFilterOperator("equals", "equals", true), - makeGridFilterOperator("isNot", "not equals", true), + makeGridFilterOperator("isNot", "does not equal", true), makeGridFilterOperator("after", "is after", true), makeGridFilterOperator("onOrAfter", "is on or after", true), makeGridFilterOperator("before", "is before", true), makeGridFilterOperator("onOrBefore", "is on or before", true), makeGridFilterOperator("isEmpty", "is empty"), makeGridFilterOperator("isNotEmpty", "is not empty"), + makeGridFilterOperator("between", "is between", true), + makeGridFilterOperator("notBetween", "is not between", true), +]; + +const QGridDateTimeOperators = [ + makeGridFilterOperator("equals", "equals", true), + makeGridFilterOperator("isNot", "does not equal", true), + makeGridFilterOperator("after", "is after", true), + makeGridFilterOperator("onOrAfter", "is at or after", true), + makeGridFilterOperator("before", "is before", true), + makeGridFilterOperator("onOrBefore", "is at or before", true), + makeGridFilterOperator("isEmpty", "is empty"), + makeGridFilterOperator("isNotEmpty", "is not empty"), + makeGridFilterOperator("between", "is between", true), + makeGridFilterOperator("notBetween", "is not between", true), ]; export default class DataGridUtils @@ -272,7 +290,7 @@ export default class DataGridUtils case QFieldType.DATE_TIME: columnType = "dateTime"; columnWidth = 200; - filterOperators = QGridDateOperators; + filterOperators = QGridDateTimeOperators; break; case QFieldType.BOOLEAN: columnType = "string"; // using boolean gives an odd 'no' for nulls. diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index d3c4372..8ce5706 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -446,107 +446,15 @@ class FilterUtils } } - // todo - use expressions here!! - if (field && field.type == "DATE_TIME" && !values) + ////////////////////////////////////////////////////////////////////////// + // replace objects that look like expressions with expression instances // + ////////////////////////////////////////////////////////////////////////// + for(let i = 0; i < values.length; i++) { - try + const expression = this.gridCriteriaValueToExpression(values[i]) + if(expression) { - const criteria = filterJSON.criteria[i]; - if (criteria && criteria.expression) - { - let value = new Date(); - let amount = Number(criteria.expression.amount); - switch (criteria.expression.timeUnit) - { - case "MINUTES": - { - amount = amount * 60; - break; - } - case "HOURS": - { - amount = amount * 60 * 60; - break; - } - case "DAYS": - { - amount = amount * 60 * 60 * 24; - break; - } - default: - { - console.log("Unrecognized time unit: " + criteria.expression.timeUnit); - } - } - - if (criteria.expression.operator == "MINUS") - { - amount = -amount; - } - - ///////////////////////////////////////////// - // shift the date/time by the input amount // - ///////////////////////////////////////////// - value.setTime(value.getTime() + 1000 * amount); - - ///////////////////////////////////////////////// - // now also shift from local-timezone into UTC // - ///////////////////////////////////////////////// - value.setTime(value.getTime() + 1000 * 60 * value.getTimezoneOffset()); - - values = [ValueUtils.formatDateTimeISO8601(value)]; - } - } - catch (e) - { - console.log(e); - } - } - - if (field && field.type == "DATE" && !values) - { - try - { - const criteria = filterJSON.criteria[i]; - if (criteria && criteria.expression) - { - let value = new Date(); - let amount = Number(criteria.expression.amount); - switch (criteria.expression.timeUnit) - { - case "MINUTES": - { - amount = amount * 60; - break; - } - case "HOURS": - { - amount = amount * 60 * 60; - break; - } - case "DAYS": - { - amount = amount * 60 * 60 * 24; - break; - } - default: - { - console.log("Unrecognized time unit: " + criteria.expression.timeUnit); - } - } - - if (criteria.expression.operator == "MINUS") - { - amount = -amount; - } - - value.setTime(value.getTime() + 1000 * amount); - values = [ValueUtils.formatDateISO8601(value)]; - } - } - catch (e) - { - console.log(e); + values[i] = expression; } } @@ -554,7 +462,7 @@ class FilterUtils columnField: criteria.fieldName, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values), value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field), - id: id++, // not sure what this id is!! + id: id++ }); } @@ -564,9 +472,9 @@ class FilterUtils defaultFilter.linkOperator = GridLinkOperator.Or; } - ////////////////////////////////////////////////////////////////// - // translate from a qqq-style filter to one that the grid wants // - ////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////// + // translate from qqq-style orderBy to one that the grid wants // + ///////////////////////////////////////////////////////////////// if (qQueryFilter.orderBys && qQueryFilter.orderBys.length > 0) { for (let i = 0; i < qQueryFilter.orderBys.length; i++) @@ -610,14 +518,32 @@ class FilterUtils } } + ///////////////////////////////////////////////////////////////////////////////// + // if any values in the items are objects, but should be expression instances, // + // then convert & replace them. // + ///////////////////////////////////////////////////////////////////////////////// if(defaultFilter && defaultFilter.items && defaultFilter.items.length) { defaultFilter.items.forEach((item) => { - const expression = this.gridCriteriaValueToExpression(item.value) - if(expression) + if(item.value && item.value.length) { - item.value = expression; + for (let i = 0; i < item.value.length; i++) + { + const expression = this.gridCriteriaValueToExpression(item.value[i]) + if(expression) + { + item.value[i] = expression; + } + } + } + else + { + const expression = this.gridCriteriaValueToExpression(item.value) + if(expression) + { + item.value = expression; + } } }); } @@ -726,14 +652,24 @@ class FilterUtils //////////////////////////////////////////////////////////////////////////////// // if no value set and not 'empty' or 'not empty' operators, skip this filter // //////////////////////////////////////////////////////////////////////////////// - if ((!item.value || item.value.length == 0 || (item.value.length == 1 && (item.value[0] === "" || item.value[0] === undefined))) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty") + let incomplete = false; + if (item.operatorValue === "between" || item.operatorValue === "notBetween") { - if (!allowIncompleteCriteria) + if(!item.value || !item.value.length || item.value.length < 2 || this.isUnset(item.value[0]) || this.isUnset(item.value[1])) { - console.log(`Discarding incomplete filter criteria: ${JSON.stringify(item)}`); - return; + incomplete = true; } } + else if ((!item.value || item.value.length == 0 || (item.value.length == 1 && this.isUnset(item.value[0]))) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty") + { + incomplete = true; + } + + if (incomplete && !allowIncompleteCriteria) + { + console.log(`Discarding incomplete filter criteria: ${JSON.stringify(item)}`); + return; + } const fieldMetadata = tableMetaData?.fields.get(item.columnField); const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue); @@ -757,27 +693,38 @@ class FilterUtils }; + /******************************************************************************* + ** + *******************************************************************************/ + private static isUnset(value: any) + { + return value === "" || value === undefined; + } + /******************************************************************************* ** *******************************************************************************/ private static gridCriteriaValueToExpression(value: any) { - if (value.length) + if (value && value.length) { value = value[0]; } - if (value.type && value.type == "NowWithOffset") + if (value && value.type) { - return (new NowWithOffsetExpression(value)); - } - else if (value.type && value.type == "Now") - { - return (new NowExpression(value)); - } - else if (value.type && value.type == "ThisOrLastPeriod") - { - return (new ThisOrLastPeriodExpression(value)); + if (value.type == "NowWithOffset") + { + return (new NowWithOffsetExpression(value)); + } + else if (value.type == "Now") + { + return (new NowExpression(value)); + } + else if (value.type == "ThisOrLastPeriod") + { + return (new ThisOrLastPeriodExpression(value)); + } } return (null); diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index ad9695c..909ec04 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -254,6 +254,16 @@ class ValueUtils return (returnValue); } + public static formatDate(date: Date) + { + if (!(date instanceof Date)) + { + date = new Date(date); + } + // @ts-ignore + return (`${date.toString("yyyy-MM-dd")}`); + } + public static formatDateTime(date: Date) { if (!(date instanceof Date)) From 2e4f787462816ca9f5aa7101ea63521e61199eb6 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 17 Jul 2023 08:54:09 -0500 Subject: [PATCH 06/11] Update qqq-frontend-core to 1.0.79 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca50ab1..bf637c9 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.78", + "@kingsrook/qqq-frontend-core": "1.0.79", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", From 0a0385867cf188f4001417ec2efa975190123819 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 17 Jul 2023 08:54:09 -0500 Subject: [PATCH 07/11] Update qqq-frontend-core to 1.0.79 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca50ab1..bf637c9 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.78", + "@kingsrook/qqq-frontend-core": "1.0.79", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", From a3228166fb570c2909d8f3ea78cf6b33bede3792 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 17 Jul 2023 16:12:21 -0500 Subject: [PATCH 08/11] cleanup/resort vars --- src/QContext.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/QContext.tsx b/src/QContext.tsx index 98274d1..2cd7ca5 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -26,21 +26,26 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {createContext} from "react"; - interface QContext { pageHeader: string | JSX.Element; setPageHeader?: (header: string | JSX.Element) => void; + accentColor: string; setAccentColor?: (header: string) => void; + dotMenuOpen: boolean; setDotMenuOpen?: (dotMenuOpen: boolean) => void; - qInstance?: QInstance; - appMetaData?: QAppMetaData; + tableMetaData?: QTableMetaData; setTableMetaData?: (tableMetaData: QTableMetaData) => void; + tableProcesses?: QProcessMetaData[]; setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void; + + /////////////////////////////////// + // constants - no setters needed // + /////////////////////////////////// pathToLabelMap?: {[path: string]: string}; branding?: QBrandingMetaData; } @@ -53,4 +58,4 @@ const defaultState = { }; const QContext = createContext(defaultState); -export default QContext; +export default QContext; \ No newline at end of file From 95144608e5dc0f5f170b43175b4e53c242189444 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 19 Jul 2023 11:16:59 -0500 Subject: [PATCH 09/11] Strip trailing slashes from routes / breadcrumbs / titles --- src/qqq/components/horseshoe/Breadcrumbs.tsx | 19 +++++++++++++++++++ src/qqq/components/horseshoe/NavBar.tsx | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/src/qqq/components/horseshoe/Breadcrumbs.tsx b/src/qqq/components/horseshoe/Breadcrumbs.tsx index b4fc4ec..f3006a4 100644 --- a/src/qqq/components/horseshoe/Breadcrumbs.tsx +++ b/src/qqq/components/horseshoe/Breadcrumbs.tsx @@ -59,11 +59,25 @@ export const routeToLabel = (route: string): string => function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element { + /////////////////////////////////////////////////////////////////////// + // strip away empty elements of the route (e.g., trailing slash(es)) // + /////////////////////////////////////////////////////////////////////// + if(route.length) + { + // @ts-ignore + route = route.filter(r => r != ""); + } + const routes: string[] | any = route.slice(0, -1); const {pageHeader, pathToLabelMap, branding} = useContext(QContext); const fullPathToLabel = (fullPath: string, route: string): string => { + if(fullPath.endsWith("/")) + { + fullPath = fullPath.replace(/\/+$/, ""); + } + if(pathToLabelMap && pathToLabelMap[fullPath]) { return pathToLabelMap[fullPath]; @@ -82,6 +96,11 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element continue; } + if(routes[i] === "") + { + continue; + } + accumulatedPath = `${accumulatedPath}/${routes[i]}`; fullRoutes.push(accumulatedPath); pageTitle = `${fullPathToLabel(accumulatedPath, routes[i])} | ${pageTitle}`; diff --git a/src/qqq/components/horseshoe/NavBar.tsx b/src/qqq/components/horseshoe/NavBar.tsx index 304d287..3e15f5d 100644 --- a/src/qqq/components/horseshoe/NavBar.tsx +++ b/src/qqq/components/horseshoe/NavBar.tsx @@ -215,6 +215,11 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element const {pathToLabelMap} = useContext(QContext); const fullPathToLabel = (fullPath: string, route: string): string => { + if(fullPath.endsWith("/")) + { + fullPath = fullPath.replace(/\/+$/, ""); + } + if(pathToLabelMap && pathToLabelMap[fullPath]) { return pathToLabelMap[fullPath]; From aa4379787ddba8c18ffe7c25cc5b3dda7b72a061 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 20 Jul 2023 08:07:24 -0500 Subject: [PATCH 10/11] Try specific chrome version - new one appears to not be supported by browser tools? --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d49e14..965a988 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,8 @@ commands: mvn_verify: steps: - - browser-tools/install-chrome + - browser-tools/install-chrome: + chrome-version: 114.0.5735.198 - browser-tools/install-chromedriver - run: name: install dockerize From 70b2b70ba0f9c5653d2de910ec28e14085df5cfd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 20 Jul 2023 08:11:55 -0500 Subject: [PATCH 11/11] Try updating orb rather than specific chrome version --- .circleci/config.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 965a988..7da77eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 orbs: node: circleci/node@5.1.0 - browser-tools: circleci/browser-tools@1.4.1 + browser-tools: circleci/browser-tools@1.4.3 executors: java17: @@ -29,8 +29,7 @@ commands: mvn_verify: steps: - - browser-tools/install-chrome: - chrome-version: 114.0.5735.198 + - browser-tools/install-chrome - browser-tools/install-chromedriver - run: name: install dockerize