Merge pull request #25 from Kingsrook/integration/sprint-29

Integration/sprint 29
This commit is contained in:
2023-07-20 09:55:53 -05:00
committed by GitHub
15 changed files with 1117 additions and 214 deletions

View File

@ -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:

View File

@ -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",

View File

@ -33,7 +33,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";
@ -149,6 +149,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 //
@ -467,6 +468,14 @@ export default function App()
});
}
const pathToLabelMap: {[path: string]: string} = {}
for(let i =0; i<appRoutesList.length; i++)
{
const route = appRoutesList[i];
pathToLabelMap[route.route] = route.name;
}
setPathToLabelMap(pathToLabelMap);
const newSideNavRoutes = [];
// @ts-ignore
newSideNavRoutes.unshift(profileRoutes);
@ -578,7 +587,9 @@ export default function App()
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses),
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent)
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent),
pathToLabelMap: pathToLabelMap,
branding: branding
}}>
<ThemeProvider theme={theme}>
<CssBaseline />
@ -590,6 +601,7 @@ export default function App()
appName={branding.appName}
branding={branding}
routes={sideNavRoutes}
pathToLabelMap={pathToLabelMap}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
/>

View File

@ -20,32 +20,41 @@
*/
import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData";
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {createContext} from "react";
interface QContext
{
pageHeader: string | JSX.Element;
setPageHeader?: (header: string | JSX.Element) => void;
accentColor: string;
setAccentColor?: (header: string) => void;
dotMenuOpen: boolean;
qInstance?: QInstance;
appMetaData?: QAppMetaData;
setDotMenuOpen?: (dotMenuOpen: boolean) => void;
tableMetaData?: QTableMetaData;
setTableMetaData?: (tableMetaData: QTableMetaData) => void;
tableProcesses?: QProcessMetaData[];
setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void;
setDotMenuOpen?: (dotMenuOpen: boolean) => void;
///////////////////////////////////
// constants - no setters needed //
///////////////////////////////////
pathToLabelMap?: {[path: string]: string};
branding?: QBrandingMetaData;
}
const defaultState = {
pageHeader: "",
accentColor: "#0062FF",
dotMenuOpen: false
dotMenuOpen: false,
pathToLabelMap: {},
};
const QContext = createContext<QContext>(defaultState);

View File

@ -59,10 +59,34 @@ 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);
///////////////////////////////////////////////////////////////////////
// strip away empty elements of the route (e.g., trailing slash(es)) //
///////////////////////////////////////////////////////////////////////
if(route.length)
{
// @ts-ignore
route = route.filter(r => r != "");
}
let pageTitle = "ColdTrack Live";
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];
}
return (routeToLabel(route));
}
let pageTitle = branding?.appName ?? "";
const fullRoutes: string[] = [];
let accumulatedPath = "";
for (let i = 0; i < routes.length; i++)
@ -72,9 +96,14 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
continue;
}
if(routes[i] === "")
{
continue;
}
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 +139,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(/.*\//, ""))}
</MDTypography>
</Link>
))}

View File

@ -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<any>(false);
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
const fullPath = useLocation().pathname;
const route = useLocation().pathname.split("/").slice(1);
const navigate = useNavigate();
@ -210,7 +212,23 @@ 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(fullPath.endsWith("/"))
{
fullPath = fullPath.replace(/\/+$/, "");
}
if(pathToLabelMap && pathToLabelMap[fullPath])
{
return pathToLabelMap[fullPath];
}
return (routeToLabel(route));
}
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
return (
<AppBar

View File

@ -0,0 +1,312 @@
/*
* 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 {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";
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: QFieldType
expression: any;
onSave: (expression: any) => void;
forcedOpen: boolean;
}
AdvancedDateTimeFilterValues.defaultProps = {
forcedOpen: false
};
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, forcedOpen}: Props): JSX.Element
{
const [originalExpression, setOriginalExpression] = useState(JSON.stringify(expression));
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 [isOpen, setIsOpen] = useState(false)
if(!isOpen && forcedOpen)
{
setIsOpen(true);
}
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)
{
///////////////////////////////////////////////////////////
// update all state vars based on the current expression //
///////////////////////////////////////////////////////////
setStateToExpression(expression);
setOriginalExpression(JSON.stringify(expression))
}
const openDialog = () =>
{
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 = 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}`);
}
}
close();
}
const close = () =>
{
setIsOpen(false);
}
function handleExpressionTypeChange(e: React.ChangeEvent<HTMLInputElement>)
{
setExpressionType(e.target.value);
}
function handleNowWithOffsetAmountChange(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>)
{
setNowWithOffsetAmount(parseInt(event.target.value));
}
function handleNowWithOffsetTimeUnitChange(event: SelectChangeEvent<NowWithOffsetUnit>, child: ReactNode)
{
// @ts-ignore
setNowWithOffsetTimeUnit(event.target.value)
}
function handleNowWithOffsetOperatorChange(event: SelectChangeEvent<NowWithOffsetOperator>, child: ReactNode)
{
// @ts-ignore
setNowWithOffsetOperator(event.target.value)
}
function handleThisOrLastPeriodTimeUnitChange(event: SelectChangeEvent<ThisOrLastPeriodUnit>, child: ReactNode)
{
// @ts-ignore
setThisOrLastPeriodTimeUnit(event.target.value)
}
function handleThisOrLastPeriodOperatorChange(event: SelectChangeEvent<ThisOrLastPeriodOperator>, 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 nwoTUs = (nowWithOffsetAmount == 1 ? "" : "s");
return (
<Box>
<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", position: "relative", top: "2px"}}>settings</Icon>
</Tooltip>
{
isOpen &&
(
<Modal open={isOpen} className="AdvancedDateTimeFilterValues">
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
<Card sx={mainCardStyles}>
<Box p={4} pb={2}>
<Grid container>
<Grid item pr={3} xs={12} lg={12}>
<Typography variant="h5">Custom Date Filter Condition</Typography>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
Select the type of expression you want for your condition.<br />
Then enter values to express your condition.
</Typography>
</Grid>
</Grid>
</Box>
<RadioGroup name="expressionType" value={expressionType} onChange={handleExpressionTypeChange}>
<Box px={4} pb={4}>
<FormControlLabel value="Now" control={<Radio size="small" />} label={type == QFieldType.DATE_TIME ? "Now" : "Today"} />
</Box>
<Box px={4} pb={4}>
<FormControlLabel value="NowWithOffset" control={<Radio size="small" />} label="Relative Expression" />
<Box pl={4}>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>
<TextField
variant="standard"
type="number"
inputProps={{min: 0}}
autoComplete="off"
value={nowWithOffsetAmount}
onChange={(event) => handleNowWithOffsetAmountChange(event)}
fullWidth
/>
</FormControl>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>
<Select value={nowWithOffsetTimeUnit} disabled={false} onChange={handleNowWithOffsetTimeUnitChange} label="Unit">
{type == QFieldType.DATE_TIME && <MenuItem value="SECONDS">Second{nwoTUs}</MenuItem>}
{type == QFieldType.DATE_TIME && <MenuItem value="MINUTES">Minute{nwoTUs}</MenuItem>}
{type == QFieldType.DATE_TIME && <MenuItem value="HOURS">Hour{nwoTUs}</MenuItem>}
<MenuItem value="DAYS">Day{nwoTUs}</MenuItem>
<MenuItem value="WEEKS">Week{nwoTUs}</MenuItem>
<MenuItem value="MONTHS">Month{nwoTUs}</MenuItem>
<MenuItem value="YEARS">Year{nwoTUs}</MenuItem>
</Select>
</FormControl>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "40%"}}>
<Select value={nowWithOffsetOperator} disabled={false} onChange={handleNowWithOffsetOperatorChange}>
<MenuItem value="MINUS">Ago (in the past)</MenuItem>
<MenuItem value="PLUS">From now (in the future)</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
<Box px={4} pb={4}>
<FormControlLabel value="ThisOrLastPeriod" control={<Radio size="small" />} label={`${type == QFieldType.DATE_TIME ? "Start of " : ""}This or Last...`} />
<Box pl={4}>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>
<Select value={thisOrLastPeriodOperator} disabled={false} onChange={handleThisOrLastPeriodOperatorChange}>
<MenuItem value="THIS">This</MenuItem>
<MenuItem value="LAST">Last</MenuItem>
</Select>
</FormControl>
<FormControl variant="standard" sx={{verticalAlign: "bottom", width: "30%"}}>
<Select value={thisOrLastPeriodTimeUnit} disabled={false} onChange={handleThisOrLastPeriodTimeUnitChange} label="Unit">
{type == QFieldType.DATE_TIME && <MenuItem value="HOURS">Hour</MenuItem>}
<MenuItem value="DAYS">Day</MenuItem>
<MenuItem value="WEEKS">Week</MenuItem>
<MenuItem value="MONTHS">Month</MenuItem>
<MenuItem value="YEARS">Year</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
</RadioGroup>
<Box p={3} pt={0}>
<Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
<QCancelButton onClickHandler={handleCancelClicked} iconName="cancel" disabled={false} />
<QSaveButton onClickHandler={handleSaveClicked} label="Apply" disabled={false} />
</Grid>
</Box>
</Card>
</Box>
</Box>
</Modal>
)
}
</Box>
);
}
export default AdvancedDateTimeFilterValues;

View File

@ -0,0 +1,274 @@
/*
* 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 {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 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, {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;
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 [forceAdvancedDateTimeDialogOpen, setForceAdvancedDateTimeDialogOpen] = useState(false)
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const openRelativeDateTimeMenu = (event: React.MouseEvent<HTMLElement>) =>
{
setRelativeDateTimeMenuAnchorElement(event.currentTarget);
};
const closeRelativeDateTimeMenu = () =>
{
setRelativeDateTimeMenuAnchorElement(null);
};
const setExpression = (valueIndex: number, expression: Expression) =>
{
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<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{
valueChangeHandler(event, index, "");
forceUpdate();
document.getElementById(`${idPrefix}${criteria.id}`).focus();
};
const inputProps: any = {};
inputProps.endAdornment = (
<InputAdornment position="end">
<IconButton sx={{visibility: expression ? "visible" : "hidden"}} onClick={(event) => clearValue(event, valueIndex)}>
<Icon>close</Icon>
</IconButton>
</InputAdornment>
);
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;
}
}
if (expression?.type == "Now")
{
if (field.type == QFieldType.DATE)
{
displayValue = "today";
}
}
return <NoWrapTooltip title={<EvaluatedExpression field={field} expression={expression} />} placement="bottom" enterDelay={1000} sx={{marginLeft: "-75px !important", marginTop: "-8px !important"}}><TextField
id={`${idPrefix}${criteria.id}`}
label={label}
variant="standard"
autoComplete="off"
InputProps={{readOnly: true, unselectable: "off", ...inputProps}}
InputLabelProps={{shrink: true}}
value={displayValue}
fullWidth
/></NoWrapTooltip>;
};
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) => (
<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">
{
isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
: makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix)
}
<Box>
<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", position: "relative", top: "2px"}} onClick={openRelativeDateTimeMenu}>date_range</Icon>
</Tooltip>
<Menu
open={relativeDateTimeMenuAnchorElement}
anchorEl={relativeDateTimeMenuAnchorElement}
transformOrigin={{horizontal: "left", vertical: "top"}}
onClose={closeRelativeDateTimeMenu}
>
{
field.type == QFieldType.DATE ?
<Box display="flex">
<Box>
{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"))}
<Divider />
<Tooltip title="Define a custom expression" placement="left">
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
</Tooltip>
</Box>
<Box>
{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"))}
</Box>
</Box>
:
<Box display="flex">
<Box>
{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"))}
<Divider />
<Tooltip title="Define a custom expression" placement="left">
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
</Tooltip>
</Box>
<Box>
{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"))}
</Box>
</Box>
}
</Menu>
</Box>
<Box>
<AdvancedDateTimeFilterValues type={field.type} expression={currentExpression} onSave={(expression: any) => saveNewDateTimeExpression(valueIndex, expression)} forcedOpen={forceAdvancedDateTimeDialogOpen} />
</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

@ -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",
}
@ -159,9 +161,13 @@ 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, 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});
@ -171,11 +177,13 @@ 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, 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;
@ -447,9 +455,9 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
// don't need to look at values //
//////////////////////////////////
}
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE)
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE_TIME)
{
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.";

View File

@ -31,6 +31,7 @@ import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
import TextField from "@mui/material/TextField";
import React, {SyntheticEvent, useReducer} from "react";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import CriteriaDateField from "qqq/components/query/CriteriaDateField";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
@ -45,19 +46,9 @@ interface Props
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
}
FilterCriteriaRowValues.defaultProps = {
};
FilterCriteriaRowValues.defaultProps = {};
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!operatorOption)
{
return <br />;
}
const getTypeForTextField = (): string =>
export const getTypeForTextField = (field: QFieldMetaData): string =>
{
let type = "search";
@ -77,9 +68,9 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
return (type);
};
const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
{
let type = getTypeForTextField();
let type = getTypeForTextField(field);
const inputLabelProps: any = {};
if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME)
@ -122,6 +113,15 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
/>;
};
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!operatorOption)
{
return <br />;
}
function saveNewPasterValues(newValues: any[])
{
if (criteria.values)
@ -150,18 +150,28 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
case ValueMode.NONE:
return <br />;
case ValueMode.SINGLE:
return makeTextField();
return makeTextField(field, criteria, valueChangeHandler);
case ValueMode.SINGLE_DATE:
return makeTextField();
return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} />;
case ValueMode.DOUBLE_DATE:
return <Box>
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" />
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" />
</Box>;
case ValueMode.SINGLE_DATE_TIME:
return makeTextField();
return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} />;
case ValueMode.DOUBLE_DATE_TIME:
return <Box>
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" />
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" />
</Box>;
case ValueMode.DOUBLE:
return <Box>
<Box width="50%" display="inline-block">
{ makeTextField(0, "From", "from-") }
{makeTextField(field, criteria, valueChangeHandler, 0, "From", "from-")}
</Box>
<Box width="50%" display="inline-block">
{makeTextField(1, "To", "to-")}
{makeTextField(field, criteria, valueChangeHandler, 1, "To", "to-")}
</Box>
</Box>;
case ValueMode.MULTI:
@ -184,7 +194,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
onChange={(event, value) => valueChangeHandler(event, "all", value)}
/>
<Box>
<FilterCriteriaPaster type={getTypeForTextField()} onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
<FilterCriteriaPaster type={getTypeForTextField(field)} onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
</Box>
</Box>;
case ValueMode.PVS_SINGLE:
@ -233,7 +243,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
/>
</Box>
</Box>;
}
return (<br />);

View File

@ -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 */

View File

@ -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.

View File

@ -23,10 +23,13 @@ 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";
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 +288,11 @@ class FilterUtils
return (param);
}
if (FilterUtils.gridCriteriaValueToExpression(param))
{
return (param);
}
let rs = [];
for (let i = 0; i < param.length; i++)
{
@ -342,18 +350,24 @@ 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. //
////////////////////////////////////////////////////////////////////////////////////////////////
if (fieldType === QFieldType.DATE_TIME)
{
values[0] = ValueUtils.formatDateTimeValueForForm(values[0]);
for(let i = 0; i<values.length; i++)
{
if(!values[i].type)
{
values[i] = ValueUtils.formatDateTimeValueForForm(values[i]);
}
}
}
}
return (values[0]);
return (values ? values[0] : "");
};
@ -432,106 +446,15 @@ class FilterUtils
}
}
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;
}
}
@ -539,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++
});
}
@ -549,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++)
@ -595,6 +518,36 @@ 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) =>
{
if(item.value && item.value.length)
{
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;
}
}
});
}
return ({filter: defaultFilter, sort: defaultSort});
}
@ -699,19 +652,30 @@ 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]))
{
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);
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);
qFilter.addCriteria(criteria);
foundFilter = true;
});
@ -729,6 +693,44 @@ class FilterUtils
};
/*******************************************************************************
**
*******************************************************************************/
private static isUnset(value: any)
{
return value === "" || value === undefined;
}
/*******************************************************************************
**
*******************************************************************************/
private static gridCriteriaValueToExpression(value: any)
{
if (value && value.length)
{
value = value[0];
}
if (value && value.type)
{
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);
}
/*******************************************************************************
** edit the input filter object, replacing any values which have {id,label} attributes
** to instead just have the id part.

View File

@ -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))