mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 12:50:43 +00:00
Initial build of quick-filters on query screen
This commit is contained in:
146
src/qqq/components/misc/FieldAutoComplete.tsx
Normal file
146
src/qqq/components/misc/FieldAutoComplete.tsx
Normal 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"}}}}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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"
|
||||
|
373
src/qqq/components/query/QuickFilter.tsx
Normal file
373
src/qqq/components/query/QuickFilter.tsx
Normal 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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
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";
|
||||
@ -29,6 +30,8 @@ import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTa
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
@ -50,7 +53,7 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue, GridColumnResizeParams} from "@mui/x-data-grid-pro";
|
||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue} from "@mui/x-data-grid-pro";
|
||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||
import FormData from "form-data";
|
||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
@ -58,10 +61,12 @@ import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
||||
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||
import SavedFilters from "qqq/components/misc/SavedFilters";
|
||||
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
||||
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
||||
import {CustomFilterPanel, QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||
import QuickFilter from "qqq/components/query/QuickFilter";
|
||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
@ -83,6 +88,7 @@ const COLUMN_ORDERING_LOCAL_STORAGE_KEY_ROOT = "qqq.columnOrdering";
|
||||
const COLUMN_WIDTHS_LOCAL_STORAGE_KEY_ROOT = "qqq.columnWidths";
|
||||
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||
const QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT = "qqq.quickFilterFieldNames";
|
||||
|
||||
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
|
||||
@ -98,6 +104,7 @@ RecordQuery.defaultProps = {
|
||||
};
|
||||
|
||||
const qController = Client.getInstance();
|
||||
let debounceTimeout: string | number | NodeJS.Timeout;
|
||||
|
||||
function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
@ -144,6 +151,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||
const quickFilterFieldNamesLocalStorageKey = `${QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||
let defaultSort = [] as GridSortItem[];
|
||||
let defaultVisibility = {} as { [index: string]: boolean };
|
||||
let didDefaultVisibilityComeFromLocalStorage = false;
|
||||
@ -154,6 +162,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
let defaultColumnWidths = {} as {[fieldName: string]: number};
|
||||
let seenJoinTables: {[tableName: string]: boolean} = {};
|
||||
let defaultTableVariant: QTableVariant = null;
|
||||
let defaultQuickFilterFieldNames: Set<string> = new Set<string>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// set the to be not per table (do as above if we want per table) at a later port //
|
||||
@ -197,6 +206,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
}
|
||||
if (localStorage.getItem(quickFilterFieldNamesLocalStorageKey))
|
||||
{
|
||||
defaultQuickFilterFieldNames = new Set<string>(JSON.parse(localStorage.getItem(quickFilterFieldNamesLocalStorageKey)));
|
||||
}
|
||||
|
||||
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
||||
@ -253,6 +266,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
|
||||
const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter);
|
||||
|
||||
const [addQuickFilterMenu, setAddQuickFilterMenu] = useState(null);
|
||||
const [quickFilterFieldNames, setQuickFilterFieldNames] = useState(defaultQuickFilterFieldNames);
|
||||
const [addQuickFilterOpenCounter, setAddQuickFilterOpenCounter] = useState(0);
|
||||
|
||||
const instance = useRef({timer: null});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1946,6 +1963,129 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
);
|
||||
}
|
||||
|
||||
const updateQuickCriteria = (newCriteria: QFilterCriteria, needDebounce = false, doClearCriteria = false) =>
|
||||
{
|
||||
let found = false;
|
||||
let foundIndex = null;
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
if(queryFilter.criteria[i].fieldName == newCriteria.fieldName)
|
||||
{
|
||||
queryFilter.criteria[i] = newCriteria;
|
||||
found = true;
|
||||
foundIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(doClearCriteria)
|
||||
{
|
||||
if(found)
|
||||
{
|
||||
queryFilter.criteria.splice(foundIndex, 1);
|
||||
setQueryFilter(queryFilter);
|
||||
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
||||
handleFilterChange(gridFilterModel, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(!found)
|
||||
{
|
||||
if(!queryFilter.criteria)
|
||||
{
|
||||
queryFilter.criteria = [];
|
||||
}
|
||||
queryFilter.criteria.push(newCriteria);
|
||||
found = true;
|
||||
}
|
||||
|
||||
if(found)
|
||||
{
|
||||
clearTimeout(debounceTimeout)
|
||||
debounceTimeout = setTimeout(() =>
|
||||
{
|
||||
setQueryFilter(queryFilter);
|
||||
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
||||
handleFilterChange(gridFilterModel, false);
|
||||
}, needDebounce ? 500 : 1);
|
||||
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getQuickCriteriaParam = (fieldName: string): QFilterCriteriaWithId | null | "tooComplex" =>
|
||||
{
|
||||
const matches: QFilterCriteriaWithId[] = [];
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
if(queryFilter.criteria[i].fieldName == fieldName)
|
||||
{
|
||||
matches.push(queryFilter.criteria[i] as QFilterCriteriaWithId);
|
||||
}
|
||||
}
|
||||
|
||||
if(matches.length == 0)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
else if(matches.length == 1)
|
||||
{
|
||||
return (matches[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "tooComplex";
|
||||
}
|
||||
}
|
||||
|
||||
const toggleQuickFilterField = (fieldName: string): void =>
|
||||
{
|
||||
if(quickFilterFieldNames.has(fieldName))
|
||||
{
|
||||
quickFilterFieldNames.delete(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
quickFilterFieldNames.add(fieldName);
|
||||
}
|
||||
setQuickFilterFieldNames(new Set<string>([...quickFilterFieldNames.values()]))
|
||||
localStorage.setItem(quickFilterFieldNamesLocalStorageKey, JSON.stringify([...quickFilterFieldNames.values()]));
|
||||
|
||||
// damnit, not auto-updating in the filter panel... have to click twice most of the time w/o this hacky hack.
|
||||
setTimeout(() => forceUpdate(), 10);
|
||||
}
|
||||
|
||||
const openAddQuickFilterMenu = (event: any) =>
|
||||
{
|
||||
setAddQuickFilterMenu(event.currentTarget);
|
||||
setAddQuickFilterOpenCounter(addQuickFilterOpenCounter + 1);
|
||||
}
|
||||
|
||||
const closeAddQuickFilterMenu = () =>
|
||||
{
|
||||
setAddQuickFilterMenu(null);
|
||||
}
|
||||
|
||||
function addQuickFilterField(event: any, newValue: any, reason: string)
|
||||
{
|
||||
if(reason == "blur")
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// this keeps a click out of the menu from selecting the option //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldName = newValue ? newValue.fieldName : null
|
||||
if(fieldName)
|
||||
{
|
||||
toggleQuickFilterField(fieldName);
|
||||
closeAddQuickFilterMenu();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<div className="recordQuery">
|
||||
@ -2010,6 +2150,74 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
<QCreateNewButton tablePath={metaData?.getTablePathByName(tableName)} />
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" flexWrap="wrap" position="relative" top={"-0.5rem"} left={"0.5rem"} minHeight="2.5rem">
|
||||
<Tooltip enterDelay={1000} title={
|
||||
<Box textAlign="left">
|
||||
Fields that are frequently used for filter conditions can be added here for quick access.<br /><br />
|
||||
Use the <Icon fontSize="medium" sx={{position: "relative", top: "0.5rem"}}>add_circle_outline</Icon> button to add a field.<br /><br />
|
||||
To remove a field, click it and then use the <Icon fontSize="medium" sx={{position: "relative", top: "0.5rem"}}>highlight_off</Icon> button.
|
||||
</Box>} placement="left">
|
||||
<Typography variant="h6" sx={{cursor: "default"}}>Quick Filter:</Typography>
|
||||
</Tooltip>
|
||||
{
|
||||
metaData && tableMetaData &&
|
||||
<>
|
||||
<Tooltip enterDelay={500} title="Add a Quick Filter field" placement="top">
|
||||
<IconButton onClick={(e) => openAddQuickFilterMenu(e)} size="small" disableRipple><Icon>add_circle_outline</Icon></IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
anchorEl={addQuickFilterMenu}
|
||||
anchorOrigin={{vertical: "bottom", horizontal: "center"}}
|
||||
transformOrigin={{vertical: "top", horizontal: "left"}}
|
||||
open={Boolean(addQuickFilterMenu)}
|
||||
onClose={closeAddQuickFilterMenu}
|
||||
keepMounted
|
||||
>
|
||||
<Box width="250px">
|
||||
<FieldAutoComplete
|
||||
key={addQuickFilterOpenCounter} // use a unique key each time we open it, because we don't want the user's last selection to stick.
|
||||
id={"add-quick-filter-field"}
|
||||
metaData={metaData}
|
||||
tableMetaData={tableMetaData}
|
||||
defaultValue={null}
|
||||
handleFieldChange={addQuickFilterField}
|
||||
autoFocus={true}
|
||||
hiddenFieldNames={[...quickFilterFieldNames.values()]}
|
||||
/>
|
||||
</Box>
|
||||
</Menu>
|
||||
</>
|
||||
}
|
||||
{
|
||||
tableMetaData &&
|
||||
[...quickFilterFieldNames.values()].map((fieldName) =>
|
||||
{
|
||||
// todo - join fields...
|
||||
// todo - sometimes i want contains (client.name, for example...)
|
||||
|
||||
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS
|
||||
if(field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
||||
{
|
||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||
}
|
||||
|
||||
return (
|
||||
field && <QuickFilter
|
||||
key={fieldName}
|
||||
fullFieldName={fieldName}
|
||||
tableMetaData={tableMetaData}
|
||||
updateCriteria={updateQuickCriteria}
|
||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||
fieldMetaData={field}
|
||||
defaultOperator={defaultOperator}
|
||||
toggleQuickFilterField={toggleQuickFilterField} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
@ -2037,7 +2245,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
tableMetaData: tableMetaData,
|
||||
metaData: metaData,
|
||||
queryFilter: queryFilter,
|
||||
updateFilter: updateFilterFromFilterPanel
|
||||
updateFilter: updateFilterFromFilterPanel,
|
||||
quickFilterFieldNames: quickFilterFieldNames,
|
||||
showQuickFilterPin: true,
|
||||
toggleQuickFilterField: toggleQuickFilterField,
|
||||
}
|
||||
}}
|
||||
localeText={{
|
||||
|
@ -424,6 +424,13 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
top: -60px !important;
|
||||
}
|
||||
|
||||
/* within the data-grid, the filter-panel's container has a max-height. mirror that, and set an overflow-y */
|
||||
.MuiDataGrid-panel .customFilterPanel
|
||||
{
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* tighten the text in the field select dropdown in custom filters */
|
||||
.customFilterPanel .MuiAutocomplete-paper
|
||||
{
|
||||
@ -487,7 +494,8 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
}
|
||||
|
||||
/* change tags in any-of value fields to not be black bg with white text */
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root,
|
||||
.quickFilter.filterValuesColumn .MuiChip-root
|
||||
{
|
||||
background: none;
|
||||
color: black;
|
||||
@ -495,20 +503,23 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
}
|
||||
|
||||
/* change 'x' icon in tags in any-of value */
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon,
|
||||
.quickFilter.filterValuesColumn .MuiChip-root .MuiChip-deleteIcon
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* change tags in any-of value fields to not be black bg with white text */
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag,
|
||||
.quickFilter.filterValuesColumn .MuiAutocomplete-tag
|
||||
{
|
||||
color: #191919;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* default hover color for the 'x' to remove a tag from an 'any-of' value was white, which made it disappear */
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover,
|
||||
.quickFilter.filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover
|
||||
{
|
||||
color: lightgray;
|
||||
}
|
||||
|
Reference in New Issue
Block a user