Initial build of quick-filters on query screen

This commit is contained in:
2023-09-25 14:21:09 -05:00
parent 16a0970d25
commit 6b13a1f3dd
6 changed files with 833 additions and 154 deletions

View File

@ -0,0 +1,146 @@
/*
* 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import React, {ReactNode} from "react";
interface FieldAutoCompleteProps
{
id: string;
metaData: QInstance;
tableMetaData: QTableMetaData;
handleFieldChange: (event: any, newValue: any, reason: string) => void;
defaultValue?: {field: QFieldMetaData, table: QTableMetaData, fieldName: string};
autoFocus?: boolean
hiddenFieldNames?: string[]
}
FieldAutoComplete.defaultProps =
{
defaultValue: null,
autoFocus: false,
hiddenFieldNames: []
};
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean, hiddenFieldNames: string[])
{
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
for (let i = 0; i < sortedFields.length; i++)
{
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
if(hiddenFieldNames && hiddenFieldNames.indexOf(fieldName) > -1)
{
continue;
}
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
}
}
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, hiddenFieldNames}: FieldAutoCompleteProps): JSX.Element
{
const fieldOptions: any[] = [];
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames);
let fieldsGroupBy = null;
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
{
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const exposedJoin = tableMetaData.exposedJoins[i];
if (metaData.tables.has(exposedJoin.joinTable.name))
{
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true, hiddenFieldNames);
}
}
}
function getFieldOptionLabel(option: any)
{
/////////////////////////////////////////////////////////////////////////////////////////
// note - we're using renderFieldOption below for the actual select-box options, which //
// are always jut field label (as they are under groupings that show their table name) //
/////////////////////////////////////////////////////////////////////////////////////////
if (option && option.field && option.table)
{
if (option.table.name == tableMetaData.name)
{
return (option.field.label);
}
else
{
return (option.table.label + ": " + option.field.label);
}
}
return ("");
}
//////////////////////////////////////////////////////////////////////////////////////////////
// for options, we only want the field label (contrast with what we show in the input box, //
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
//////////////////////////////////////////////////////////////////////////////////////////////
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
{
let label = "";
if (option && option.field)
{
label = (option.field.label);
}
return (<li {...props}>{label}</li>);
}
function isFieldOptionEqual(option: any, value: any)
{
return option.fieldName === value.fieldName;
}
return (
<Autocomplete
id={id}
renderInput={(params) => (<TextField {...params} autoFocus={autoFocus} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
// @ts-ignore
defaultValue={defaultValue}
options={fieldOptions}
onChange={handleFieldChange}
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
groupBy={fieldsGroupBy}
getOptionLabel={(option) => getFieldOptionLabel(option)}
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
/>
);
}

View File

@ -34,6 +34,7 @@ import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import React, {ReactNode, SyntheticEvent, useState} from "react";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
@ -177,16 +178,74 @@ interface FilterCriteriaRowProps
updateBooleanOperator: (newValue: string) => void;
}
FilterCriteriaRow.defaultProps = {};
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean)
{
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
for (let i = 0; i < sortedFields.length; i++)
FilterCriteriaRow.defaultProps =
{
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
};
export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValue: OperatorOption)
{
let criteriaIsValid = true;
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
function isNotSet(value: any)
{
return (value === null || value == undefined || String(value).trim() === "");
}
if(!criteria)
{
criteriaIsValid = false;
criteriaStatusTooltip = "This condition is not defined.";
return {criteriaIsValid, criteriaStatusTooltip};
}
if (!criteria.fieldName)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
}
else if (!criteria.operator)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
}
else
{
if (operatorSelectedValue)
{
if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues)
{
//////////////////////////////////
// don't need to look at values //
//////////////////////////////////
}
else if (operatorSelectedValue.valueMode == ValueMode.DOUBLE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE_TIME)
{
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.";
}
}
else if (operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI)
{
if (criteria.values.length < 1 || isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
}
}
else
{
if (!criteria.values || isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
}
}
}
}
return {criteriaIsValid, criteriaStatusTooltip};
}
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
@ -195,27 +254,6 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
const [operatorInputValue, setOperatorInputValue] = useState("");
///////////////////////////////////////////////////////////////
// set up the array of options for the fields Autocomplete //
// also, a groupBy function, in case there are exposed joins //
///////////////////////////////////////////////////////////////
const fieldOptions: any[] = [];
makeFieldOptionsForTable(tableMetaData, fieldOptions, false);
let fieldsGroupBy = null;
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
{
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const exposedJoin = tableMetaData.exposedJoins[i];
if (metaData.tables.has(exposedJoin.joinTable.name))
{
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
}
}
}
////////////////////////////////////////////////////////////
// set up array of options for operator dropdown //
// only call the function to do it if we have a field set //
@ -383,111 +421,19 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
return (false);
};
function isFieldOptionEqual(option: any, value: any)
{
return option.fieldName === value.fieldName;
}
function getFieldOptionLabel(option: any)
{
/////////////////////////////////////////////////////////////////////////////////////////
// note - we're using renderFieldOption below for the actual select-box options, which //
// are always jut field label (as they are under groupings that show their table name) //
/////////////////////////////////////////////////////////////////////////////////////////
if(option && option.field && option.table)
{
if(option.table.name == tableMetaData.name)
{
return (option.field.label);
}
else
{
return (option.table.label + ": " + option.field.label);
}
}
return ("");
}
//////////////////////////////////////////////////////////////////////////////////////////////
// for options, we only want the field label (contrast with what we show in the input box, //
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
//////////////////////////////////////////////////////////////////////////////////////////////
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
{
let label = ""
if(option && option.field)
{
label = (option.field.label);
}
return (<li {...props}>{label}</li>);
}
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
{
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
}
let criteriaIsValid = true;
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
function isNotSet(value: any)
{
return (value === null || value == undefined || String(value).trim() === "");
}
if(!criteria.fieldName)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
}
else if(!criteria.operator)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
}
else
{
if(operatorSelectedValue)
{
if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues)
{
//////////////////////////////////
// don't need to look at values //
//////////////////////////////////
}
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE_TIME)
{
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.";
}
}
else if(operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI)
{
if(criteria.values.length < 1 || isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
}
}
else
{
if(!criteria.values || isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
}
}
}
}
const tooltipEnterDelay = 750;
return (
<Box className="filterCriteriaRow" pt={0.5} display="flex" alignItems="flex-end">
<Box className="filterCriteriaRow" pt={0.5} display="flex" alignItems="flex-end" pr={0.5}>
<Box display="inline-block">
<Tooltip title="Remove this condition from your filter" enterDelay={750} placement="left">
<Tooltip title="Remove this condition from your filter" enterDelay={tooltipEnterDelay} placement="left">
<IconButton onClick={removeCriteria}><Icon fontSize="small">close</Icon></IconButton>
</Tooltip>
</Box>
@ -502,24 +448,10 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
: <span />}
</Box>
<Box display="inline-block" width={250} className="fieldColumn">
<Autocomplete
id={`field-${id}`}
renderInput={(params) => (<TextField {...params} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
// @ts-ignore
defaultValue={defaultFieldValue}
options={fieldOptions}
onChange={handleFieldChange}
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
groupBy={fieldsGroupBy}
getOptionLabel={(option) => getFieldOptionLabel(option)}
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
/>
<FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange} />
</Box>
<Box display="inline-block" width={200} className="operatorColumn">
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={750}>
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={tooltipEnterDelay}>
<Autocomplete
id={"criteriaOperator"}
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
@ -546,8 +478,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
/>
</Box>
<Box display="inline-block" pl={0.5} pr={1}>
<Tooltip title={criteriaStatusTooltip} enterDelay={750} placement="right">
<Box display="inline-block">
<Tooltip title={criteriaStatusTooltip} enterDelay={tooltipEnterDelay} placement="bottom">
{
criteriaIsValid
? <Icon color="success">check</Icon>

View File

@ -44,9 +44,13 @@ interface Props
field: QFieldMetaData;
table: QTableMetaData;
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
initiallyOpenMultiValuePvs?: boolean
}
FilterCriteriaRowValues.defaultProps = {};
FilterCriteriaRowValues.defaultProps =
{
initiallyOpenMultiValuePvs: false
};
export const getTypeForTextField = (field: QFieldMetaData): string =>
{
@ -110,16 +114,17 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
InputLabelProps={inputLabelProps}
InputProps={inputProps}
fullWidth
autoFocus={true}
/>;
};
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs}: Props): JSX.Element
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!operatorOption)
{
return <br />;
return null;
}
function saveNewPasterValues(newValues: any[])
@ -148,7 +153,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
switch (operatorOption.valueMode)
{
case ValueMode.NONE:
return <br />;
return null;
case ValueMode.SINGLE:
return makeTextField(field, criteria, valueChangeHandler);
case ValueMode.SINGLE_DATE:
@ -241,6 +246,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
isMultiple
fieldLabel="Values"
initialValues={initialValues}
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard"

View File

@ -0,0 +1,373 @@
/*
* 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {Badge, Tooltip} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import TextField from "@mui/material/TextField";
import React, {SyntheticEvent, useState} from "react";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import {getDefaultCriteriaValue, getOperatorOptions, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import TableUtils from "qqq/utils/qqq/TableUtils";
type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
interface QuickFilterProps
{
tableMetaData: QTableMetaData;
fullFieldName: string;
fieldMetaData: QFieldMetaData;
criteriaParam: CriteriaParamType;
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean, doRemoveCriteria: boolean) => void;
defaultOperator?: QCriteriaOperator;
toggleQuickFilterField?: (fieldName: string) => void;
}
const criteriaParamIsCriteria = (param: CriteriaParamType): boolean =>
{
return (param != null && param != "tooComplex");
};
QuickFilter.defaultProps =
{
defaultOperator: QCriteriaOperator.EQUALS,
toggleQuickFilterField: null
};
let seedId = new Date().getTime() % 173237;
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption =>
{
if(criteria)
{
const filteredOptions = operatorOptions.filter(o => o.value == criteria.operator);
if(filteredOptions.length > 0)
{
return (filteredOptions[0]);
}
}
const filteredOptions = operatorOptions.filter(o => o.value == defaultOperator);
if(filteredOptions.length > 0)
{
return (filteredOptions[0]);
}
return (null);
}
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, toggleQuickFilterField}: QuickFilterProps): JSX.Element
{
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
const tableForField = tableMetaData; // todo!! const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? criteriaParam as QFilterCriteriaWithId : null);
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator);
if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue))
{
setOperatorSelectedValue(maybeNewOperatorSelectedValue)
setOperatorInputValue(maybeNewOperatorSelectedValue.label)
}
if(!fieldMetaData)
{
return (null);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a change to the criteria from outside this component (e.g., the prop isn't the same as the state) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
{
const newCriteria = criteriaParam as QFilterCriteriaWithId;
setCriteria(newCriteria);
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
setOperatorSelectedValue(operatorOption);
setOperatorInputValue(operatorOption.label);
}
const criteriaNeedsReset = (): boolean =>
{
if(criteria != null && criteriaParam == null)
{
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
if(criteria.operator !== defaultOperatorOption.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
{
return (true);
}
}
return (false);
}
const makeNewCriteria = (): QFilterCriteria =>
{
const operatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
const criteria = new QFilterCriteriaWithId(fullFieldName, operatorOption.value, getDefaultCriteriaValue());
criteria.id = id;
setOperatorSelectedValue(operatorOption);
setOperatorInputValue(operatorOption.label);
setCriteria(criteria);
return(criteria);
}
if (criteria == null || criteriaNeedsReset())
{
makeNewCriteria();
}
const toggleOpen = (event: any) =>
{
setIsOpen(!isOpen);
setAnchorEl(event.currentTarget);
};
const closeMenu = () =>
{
setIsOpen(false);
setAnchorEl(null);
};
/////////////////////////////////////////////
// event handler for operator Autocomplete //
// todo - too dupe?
/////////////////////////////////////////////
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
{
criteria.operator = newValue ? newValue.value : null;
if (newValue)
{
setOperatorSelectedValue(newValue);
setOperatorInputValue(newValue.label);
if (newValue.implicitValues)
{
criteria.values = newValue.implicitValues;
}
}
else
{
setOperatorSelectedValue(null);
setOperatorInputValue("");
}
updateCriteria(criteria, false, false);
};
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
{
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
}
//////////////////////////////////////////////////
// event handler for value field (of all types) //
// todo - too dupe!
//////////////////////////////////////////////////
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
{
// @ts-ignore
const value = newValue !== undefined ? newValue : event ? event.target.value : null;
if (!criteria.values)
{
criteria.values = [];
}
if (valueIndex == "all")
{
criteria.values = value;
}
else
{
criteria.values[valueIndex] = value;
}
updateCriteria(criteria, true, false);
};
const noop = () =>
{
};
const getValuesString = (): string =>
{
let valuesString = "";
if (criteria.values && criteria.values.length)
{
let labels = [] as string[];
let maxLoops = criteria.values.length;
if (maxLoops > 5)
{
maxLoops = 3;
}
for (let i = 0; i < maxLoops; i++)
{
if (criteria.values[i] && criteria.values[i].label)
{
labels.push(criteria.values[i].label);
}
else
{
labels.push(criteria.values[i]);
}
}
if (maxLoops < criteria.values.length)
{
labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
}
valuesString = (labels.join(", "));
}
return valuesString;
}
const [startIconName, setStartIconName] = useState("filter_alt");
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
const resetCriteria = (e: React.MouseEvent<HTMLSpanElement>) =>
{
if(criteriaIsValid)
{
e.stopPropagation();
const newCriteria = makeNewCriteria();
updateCriteria(newCriteria, false, true);
setStartIconName("filter_alt");
}
}
const startIconMouseOver = () =>
{
if(criteriaIsValid)
{
setStartIconName("clear");
}
}
const startIconMouseOut = () =>
{
setStartIconName("filter_alt");
}
const tooComplex = criteriaParam == "tooComplex";
const tooltipEnterDelay = 500;
let startIcon = <Badge badgeContent={criteriaIsValid && !tooComplex ? 1 : 0} color="warning" variant="dot" onMouseOver={startIconMouseOver} onMouseOut={startIconMouseOut} onClick={resetCriteria}><Icon>{startIconName}</Icon></Badge>
if(criteriaIsValid)
{
startIcon = <Tooltip title={"Remove this condition"} enterDelay={tooltipEnterDelay}>{startIcon}</Tooltip>
}
let buttonContent = <span>{tableForField?.name != tableMetaData.name ? `${tableForField.label}: ` : ""}{fieldMetaData.label}</span>
if (criteriaIsValid)
{
buttonContent = (
<Tooltip title={`${operatorSelectedValue.label} ${getValuesString()}`} enterDelay={tooltipEnterDelay}>
{buttonContent}
</Tooltip>
);
}
let button = fieldMetaData && <Button
sx={{mr: "0.25rem", px: "1rem", border: isOpen ? "1px solid gray" : "1px solid transparent"}}
startIcon={startIcon}
onClick={tooComplex ? noop : toggleOpen}
disabled={tooComplex}
>{buttonContent}</Button>;
if (tooComplex)
{
// wrap button in span, so disabled button doesn't cause disabled tooltip
return (
<Tooltip title={`Your current filter is too complex to do a Quick Filter on ${fieldMetaData.label}. Use the Filter button to edit.`} enterDelay={tooltipEnterDelay} slotProps={{popper: {sx: {top: "-0.75rem!important"}}}}>
<span>{button}</span>
</Tooltip>
);
}
const doToggle = () =>
{
closeMenu()
toggleQuickFilterField(criteria?.fieldName);
}
const widthAndMaxWidth = 250
return (
<>
{button}
{
isOpen && <Menu open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={closeMenu} sx={{overflow: "visible"}}>
{
toggleQuickFilterField &&
<Tooltip title={"Remove this field from Quick Filters"} placement="right">
<IconButton size="small" sx={{position: "absolute", top: "-8px", right: "-8px", zIndex: 1}} onClick={doToggle}><Icon color="secondary">highlight_off</Icon></IconButton>
</Tooltip>
}
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
<Autocomplete
id={"criteriaOperator"}
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
options={operatorOptions}
value={operatorSelectedValue as any}
inputValue={operatorInputValue}
onChange={handleOperatorChange}
onInputChange={(e, value) => setOperatorInputValue(value)}
isOptionEqualToValue={(option, value) => isOperatorOptionEqual(option, value)}
getOptionLabel={(option: any) => option.label}
autoSelect={true}
autoHighlight={true}
disableClearable
slotProps={{popper: {style: {padding: 0, maxHeight: "unset", width: "250px"}}}}
/>
</Box>
<Box width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="quickFilter filterValuesColumn">
<FilterCriteriaRowValues
operatorOption={operatorSelectedValue}
criteria={criteria}
field={fieldMetaData}
table={tableMetaData} // todo - joins?
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
initiallyOpenMultiValuePvs={true} // todo - maybe not?
/>
</Box>
</Menu>
}
</>
);
}