Adding tooltips w/ evaluated real-time values; fixing bugs, getting ready for release

This commit is contained in:
2023-07-17 08:48:54 -05:00
parent 28c48cc2ef
commit 18b16ab134
6 changed files with 426 additions and 201 deletions

View File

@ -43,9 +43,12 @@ interface Props
type: QFieldType type: QFieldType
expression: any; expression: any;
onSave: (expression: any) => void; onSave: (expression: any) => void;
forcedOpen: boolean;
} }
AdvancedDateTimeFilterValues.defaultProps = {}; AdvancedDateTimeFilterValues.defaultProps = {
forcedOpen: false
};
const extractExpressionType = (expression: any) => expression?.type ?? "NowWithOffset"; const extractExpressionType = (expression: any) => expression?.type ?? "NowWithOffset";
const extractNowWithOffsetAmount = (expression: any) => expression?.type == "NowWithOffset" ? (expression?.amount ?? 1) : 1; 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 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; 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)); const [originalExpression, setOriginalExpression] = useState(JSON.stringify(expression));
@ -69,6 +72,11 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
if(!isOpen && forcedOpen)
{
setIsOpen(true);
}
const setStateToExpression = (activeExpression: any) => const setStateToExpression = (activeExpression: any) =>
{ {
setExpressionType(extractExpressionType(activeExpression)) setExpressionType(extractExpressionType(activeExpression))
@ -196,8 +204,8 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El
return ( return (
<Box> <Box>
<Tooltip title={`Define a more advanced ${type == QFieldType.DATE ? "date" : "date-time"} condition`}> <Tooltip title={`Define a custom ${type == QFieldType.DATE ? "date" : "date-time"} condition`}>
<Icon onClick={openDialog} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>settings</Icon> <Icon onClick={openDialog} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}}>settings</Icon>
</Tooltip> </Tooltip>
{ {
isOpen && isOpen &&
@ -209,7 +217,7 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El
<Box p={4} pb={2}> <Box p={4} pb={2}>
<Grid container> <Grid container>
<Grid item pr={3} xs={12} lg={12}> <Grid item pr={3} xs={12} lg={12}>
<Typography variant="h5">Advanced Date Filter Condition</Typography> <Typography variant="h5">Custom Date Filter Condition</Typography>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button"> <Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
Select the type of expression you want for your condition.<br /> Select the type of expression you want for your condition.<br />
Then enter values to express your condition. Then enter values to express your condition.
@ -220,11 +228,11 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El
<RadioGroup name="expressionType" value={expressionType} onChange={handleExpressionTypeChange}> <RadioGroup name="expressionType" value={expressionType} onChange={handleExpressionTypeChange}>
<Box px={4} pb={4}> <Box px={4} pb={4}>
<FormControlLabel value={"Now"} control={<Radio size="small" />} label="Now" /> <FormControlLabel value="Now" control={<Radio size="small" />} label={type == QFieldType.DATE_TIME ? "Now" : "Today"} />
</Box> </Box>
<Box px={4} pb={4}> <Box px={4} pb={4}>
<FormControlLabel value={"NowWithOffset"} control={<Radio size="small" />} label="Relative Expression" /> <FormControlLabel value="NowWithOffset" control={<Radio size="small" />} label="Relative Expression" />
<Box pl={4}> <Box pl={4}>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}> <FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>
<TextField <TextField
@ -260,7 +268,7 @@ function AdvancedDateTimeFilterValues({type, expression, onSave}: Props): JSX.El
</Box> </Box>
<Box px={4} pb={4}> <Box px={4} pb={4}>
<FormControlLabel value={"ThisOrLastPeriod"} control={<Radio size="small" />} label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} /> <FormControlLabel value="ThisOrLastPeriod" control={<Radio size="small" />} label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} />
<Box pl={4}> <Box pl={4}>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}> <FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>

View File

@ -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 {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 {ThisOrLastPeriodExpression, ThisOrLastPeriodOperator, ThisOrLastPeriodUnit} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import {styled} from "@mui/material/styles";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip, {tooltipClasses, TooltipProps} from "@mui/material/Tooltip";
import React, {SyntheticEvent, useReducer, useState} from "react"; import React, {SyntheticEvent, useEffect, useReducer, useState} from "react";
import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues"; import AdvancedDateTimeFilterValues from "qqq/components/query/AdvancedDateTimeFilterValues";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import {EvaluatedExpression} from "qqq/components/query/EvaluatedExpression";
import {makeTextField} from "qqq/components/query/FilterCriteriaRowValues"; import {makeTextField} from "qqq/components/query/FilterCriteriaRowValues";
export type Expression = NowWithOffsetExpression | ThisOrLastPeriodExpression | NowExpression;
interface CriteriaDateFieldProps interface CriteriaDateFieldProps
{ {
valueIndex: number; valueIndex: number;
@ -56,6 +63,7 @@ CriteriaDateField.defaultProps = {
export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler}: CriteriaDateFieldProps): JSX.Element export default function CriteriaDateField({valueIndex, label, idPrefix, field, criteria, valueChangeHandler}: CriteriaDateFieldProps): JSX.Element
{ {
const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null); const [relativeDateTimeMenuAnchorElement, setRelativeDateTimeMenuAnchorElement] = useState(null);
const [forceAdvancedDateTimeDialogOpen, setForceAdvancedDateTimeDialogOpen] = useState(false)
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const openRelativeDateTimeMenu = (event: React.MouseEvent<HTMLElement>) => const openRelativeDateTimeMenu = (event: React.MouseEvent<HTMLElement>) =>
@ -68,34 +76,9 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
setRelativeDateTimeMenuAnchorElement(null); setRelativeDateTimeMenuAnchorElement(null);
}; };
const setExpressionNow = (valueIndex: number) => const setExpression = (valueIndex: number, expression: Expression) =>
{ {
const expression = new NowExpression()
saveNewDateTimeExpression(valueIndex, expression); 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(); closeRelativeDateTimeMenu();
}; };
@ -110,7 +93,7 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) => const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{ {
valueChangeHandler(event, index, ""); valueChangeHandler(event, index, "");
forceUpdate() forceUpdate();
document.getElementById(`${idPrefix}${criteria.id}`).focus(); document.getElementById(`${idPrefix}${criteria.id}`).focus();
}; };
@ -126,13 +109,20 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
let displayValue = expression.toString(); let displayValue = expression.toString();
if (expression?.type == "ThisOrLastPeriod") 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; displayValue = "start of " + displayValue;
} }
} }
if (expression?.type == "Now")
{
if (field.type == QFieldType.DATE)
{
displayValue = "today";
}
}
return <TextField return <NoWrapTooltip title={<EvaluatedExpression field={field} expression={expression} />} placement="bottom" enterDelay={1000} sx={{marginLeft: "-75px !important", marginTop: "-8px !important"}}><TextField
id={`${idPrefix}${criteria.id}`} id={`${idPrefix}${criteria.id}`}
label={label} label={label}
variant="standard" variant="standard"
@ -141,20 +131,74 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
InputLabelProps={{shrink: true}} InputLabelProps={{shrink: true}}
value={displayValue} value={displayValue}
fullWidth fullWidth
/>; /></NoWrapTooltip>;
} };
const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type;
const currentExpression = isExpression ? criteria.values[valueIndex] : null; const currentExpression = isExpression ? criteria.values[valueIndex] : null;
const NoWrapTooltip = styled(({className, children, ...props}: TooltipProps) => (
<Tooltip {...props} classes={{popper: className}}>{children}</Tooltip>
))({
[`& .${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 <NoWrapTooltip title={<EvaluatedExpression field={field} expression={expression} />} placement={tooltipPlacement}>
<MenuItem onClick={() => setExpression(valueIndex, expression)}>{startOfPrefix}{expression.toString()}</MenuItem>
</NoWrapTooltip>;
};
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 <Box display="flex" alignItems="flex-end"> return <Box display="flex" alignItems="flex-end">
{ {
isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
: makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix) : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix)
} }
<Box> <Box>
<Tooltip title={`Choose a common relative ${field.type == QFieldType.DATE ? "date" : "date-time"} expression`} placement="top"> <Tooltip title={`Choose a common relative ${field.type == QFieldType.DATE ? "date" : "date-time"} expression`} placement="bottom">
<Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}} onClick={openRelativeDateTimeMenu}>date_range</Icon> <Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}} onClick={openRelativeDateTimeMenu}>date_range</Icon>
</Tooltip> </Tooltip>
<Menu <Menu
open={relativeDateTimeMenuAnchorElement} open={relativeDateTimeMenuAnchorElement}
@ -166,56 +210,65 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
field.type == QFieldType.DATE ? field.type == QFieldType.DATE ?
<Box display="flex"> <Box display="flex">
<Box> <Box>
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))}
<Divider />
<Tooltip title="Define a custom expression" placement="left">
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
</Tooltip>
</Box> </Box>
<Box> <Box>
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>today</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>yesterday</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))}
</Box> </Box>
</Box> </Box>
: :
<Box display="flex"> <Box display="flex">
<Box> <Box>
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 1, "HOURS")}>1 hour ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "HOURS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 12, "HOURS")}>12 hours ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 12, "HOURS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 24, "HOURS")}>24 hours ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 24, "HOURS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 7, "DAYS")}>7 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 14, "DAYS")}>14 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 30, "DAYS")}>30 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 90, "DAYS")}>90 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 180, "DAYS")}>180 days ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))}
<MenuItem onClick={() => setExpressionNowWithOffset(valueIndex, "MINUS", 1, "YEARS")}>1 year ago</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))}
<Divider />
<Tooltip title="Define a custom expression" placement="left">
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
</Tooltip>
</Box> </Box>
<Box> <Box>
<MenuItem onClick={() => setExpressionNow(valueIndex)}>now</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newNowExpression())}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "HOURS")}>start of this hour</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "HOURS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "HOURS")}>start of last hour</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "HOURS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "DAYS")}>start of today</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "DAYS")}>start of yesterday</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "WEEKS")}>start of this week</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "WEEKS")}>start of last week</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "MONTHS")}>start of this month</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "MONTHS")}>start of last month</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "THIS", "YEARS")}>start of this year</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))}
<MenuItem onClick={() => setExpressionThisOrLastPeriod(valueIndex, "LAST", "YEARS")}>start of last year</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))}
</Box> </Box>
</Box> </Box>
} }
</Menu> </Menu>
</Box> </Box>
<Box> <Box>
<AdvancedDateTimeFilterValues type={field.type} expression={currentExpression} onSave={(expression: any) => saveNewDateTimeExpression(valueIndex, expression)} /> <AdvancedDateTimeFilterValues type={field.type} expression={currentExpression} onSave={(expression: any) => saveNewDateTimeExpression(valueIndex, expression)} forcedOpen={forceAdvancedDateTimeDialogOpen} />
</Box> </Box>
</Box>; </Box>;
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
};

View File

@ -50,15 +50,33 @@ const makeGridFilterOperator = (value: string, label: string, takesValues: boole
return (rs); return (rs);
}; };
////////////////////////////////////////////////////////////////////////////////////////
// at this point, these may only be used to drive the toolitp on the FILTER button... //
////////////////////////////////////////////////////////////////////////////////////////
const QGridDateOperators = [ const QGridDateOperators = [
makeGridFilterOperator("equals", "equals", true), makeGridFilterOperator("equals", "equals", true),
makeGridFilterOperator("isNot", "not equals", true), makeGridFilterOperator("isNot", "does not equal", true),
makeGridFilterOperator("after", "is after", true), makeGridFilterOperator("after", "is after", true),
makeGridFilterOperator("onOrAfter", "is on or after", true), makeGridFilterOperator("onOrAfter", "is on or after", true),
makeGridFilterOperator("before", "is before", true), makeGridFilterOperator("before", "is before", true),
makeGridFilterOperator("onOrBefore", "is on or before", true), makeGridFilterOperator("onOrBefore", "is on or before", true),
makeGridFilterOperator("isEmpty", "is empty"), makeGridFilterOperator("isEmpty", "is empty"),
makeGridFilterOperator("isNotEmpty", "is not 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 export default class DataGridUtils
@ -272,7 +290,7 @@ export default class DataGridUtils
case QFieldType.DATE_TIME: case QFieldType.DATE_TIME:
columnType = "dateTime"; columnType = "dateTime";
columnWidth = 200; columnWidth = 200;
filterOperators = QGridDateOperators; filterOperators = QGridDateTimeOperators;
break; break;
case QFieldType.BOOLEAN: case QFieldType.BOOLEAN:
columnType = "string"; // using boolean gives an odd 'no' for nulls. columnType = "string"; // using boolean gives an odd 'no' for nulls.

View File

@ -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]; values[i] = expression;
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);
} }
} }
@ -554,7 +462,7 @@ class FilterUtils
columnField: criteria.fieldName, columnField: criteria.fieldName,
operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values), operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values),
value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field), 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; 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) if (qQueryFilter.orderBys && qQueryFilter.orderBys.length > 0)
{ {
for (let i = 0; i < qQueryFilter.orderBys.length; i++) 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) if(defaultFilter && defaultFilter.items && defaultFilter.items.length)
{ {
defaultFilter.items.forEach((item) => defaultFilter.items.forEach((item) =>
{ {
const expression = this.gridCriteriaValueToExpression(item.value) if(item.value && item.value.length)
if(expression)
{ {
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 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)}`); incomplete = true;
return;
} }
} }
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 fieldMetadata = tableMetaData?.fields.get(item.columnField);
const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue); 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) private static gridCriteriaValueToExpression(value: any)
{ {
if (value.length) if (value && value.length)
{ {
value = value[0]; value = value[0];
} }
if (value.type && value.type == "NowWithOffset") if (value && value.type)
{ {
return (new NowWithOffsetExpression(value)); if (value.type == "NowWithOffset")
} {
else if (value.type && value.type == "Now") return (new NowWithOffsetExpression(value));
{ }
return (new NowExpression(value)); else if (value.type == "Now")
} {
else if (value.type && value.type == "ThisOrLastPeriod") return (new NowExpression(value));
{ }
return (new ThisOrLastPeriodExpression(value)); else if (value.type == "ThisOrLastPeriod")
{
return (new ThisOrLastPeriodExpression(value));
}
} }
return (null); return (null);

View File

@ -254,6 +254,16 @@ class ValueUtils
return (returnValue); 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) public static formatDateTime(date: Date)
{ {
if (!(date instanceof Date)) if (!(date instanceof Date))