Checkpoint; nearing completion of custom filter panel

This commit is contained in:
2023-06-19 08:40:47 -05:00
parent 0c7330a01a
commit 50979a1ecc
9 changed files with 380 additions and 134 deletions

View File

@ -38,6 +38,7 @@ interface Props
tableName?: string;
processName?: string;
fieldName: string;
overrideId?: string;
fieldLabel: string;
inForm: boolean;
initialValue?: any;
@ -70,29 +71,34 @@ DynamicSelect.defaultProps = {
const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
{
const [ open, setOpen ] = useState(false);
const [ options, setOptions ] = useState<readonly QPossibleValue[]>([]);
const [ searchTerm, setSearchTerm ] = useState(null);
const [ firstRender, setFirstRender ] = useState(true);
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true);
////////////////////////////////////////////////////////////////////////////////////////////////
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
// else non-multiple, assume we took in an initialValue (id) and initialDisplayValue (label), //
// and build a little object that looks like a possibleValue out of those //
////////////////////////////////////////////////////////////////////////////////////////////////
const [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined)
let [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined)
: useState(initialValue && initialDisplayValue ? [{id: initialValue, label: initialDisplayValue}] : null);
if (isMultiple && defaultValue === null)
{
defaultValue = [];
}
// const loading = open && options.length === 0;
const [loading, setLoading] = useState(false);
const [ switchChecked, setSwitchChecked ] = useState(false);
const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode);
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null;
if(inForm)
if (inForm)
{
const {setFieldValue} = useFormikContext();
setFieldValueRef = setFieldValue;
@ -239,9 +245,11 @@ function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, i
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
};
// console.log(`default value: ${JSON.stringify(defaultValue)}`);
const autocomplete = (
<Autocomplete
id={fieldName}
id={overrideId ?? fieldName}
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
open={open}
fullWidth
@ -291,6 +299,8 @@ function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, i
disabled={isDisabled}
multiple={isMultiple}
disableCloseOnSelect={isMultiple}
limitTags={5}
slotProps={{popper: {className: "DynamicSelectPopper"}}}
renderInput={(params) => (
<TextField
{...params}

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
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";
@ -28,7 +29,7 @@ import Button from "@mui/material/Button/Button";
import Icon from "@mui/material/Icon/Icon";
import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro";
import React, {forwardRef, useReducer} from "react";
import {FilterCriteriaRow} from "qqq/components/query/FilterCriteriaRow";
import {FilterCriteriaRow, getDefaultCriteriaValue} from "qqq/components/query/FilterCriteriaRow";
declare module "@mui/x-data-grid"
@ -39,6 +40,7 @@ declare module "@mui/x-data-grid"
interface FilterPanelPropsOverrides
{
tableMetaData: QTableMetaData;
metaData: QInstance;
queryFilter: QQueryFilter;
updateFilter: (newFilter: QQueryFilter) => void;
}
@ -66,9 +68,9 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
{
setTimeout(() =>
{
console.log(`Try to focus ${criteriaId - 1}`);
try
{
// console.log(`Try to focus ${criteriaId - 1}`);
document.getElementById(`field-${criteriaId - 1}`).focus();
}
catch (e)
@ -80,7 +82,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
const addCriteria = () =>
{
const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, [""]);
const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, getDefaultCriteriaValue());
qFilterCriteriaWithId.id = criteriaId++;
console.log(`adding criteria id ${qFilterCriteriaWithId.id}`);
queryFilter.criteria.push(qFilterCriteriaWithId);
@ -98,8 +100,29 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
if (queryFilter.criteria.length == 0)
{
/////////////////////////////////////////////
// make sure there's at least one criteria //
/////////////////////////////////////////////
addCriteria();
}
else
{
////////////////////////////////////////////////////////////////////////////////////
// make sure all criteria have an id on them (to be used as react component keys) //
////////////////////////////////////////////////////////////////////////////////////
let updatedAny = false;
for (let i = 0; i < queryFilter.criteria.length; i++)
{
if (!queryFilter.criteria[i].id)
{
queryFilter.criteria[i].id = criteriaId++;
}
}
if (updatedAny)
{
props.updateFilter(queryFilter);
}
}
if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName)
{
@ -149,6 +172,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
id={criteria.id}
index={index}
tableMetaData={props.tableMetaData}
metaData={props.metaData}
criteria={criteria}
booleanOperator={booleanOperator}
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}

View File

@ -36,11 +36,12 @@ import ChipTextField from "qqq/components/forms/ChipTextField";
interface Props
{
type: string;
onSave: (newValues: any[]) => void;
}
FilterCriteriaPaster.defaultProps = {};
function FilterCriteriaPaster({type}: Props): JSX.Element
function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
{
enum Delimiter
{
@ -86,14 +87,6 @@ function FilterCriteriaPaster({type}: Props): JSX.Element
setPasteModalIsOpen(true);
};
const applyValue = (item: GridFilterItem) =>
{
console.log(`updating grid values: ${JSON.stringify(item.value)}`);
// todo!
// setGridFilterItem(item);
// props.applyValue(item);
};
const clearData = () =>
{
setDelimiter("");
@ -113,34 +106,19 @@ function FilterCriteriaPaster({type}: Props): JSX.Element
const handleSaveClicked = () =>
{
//x if (gridFilterItem)
/* todo
////////////////////////////////////////
// if numeric remove any non-numerics //
////////////////////////////////////////
let saveData = [];
for (let i = 0; i < chipData.length; i++)
{
////////////////////////////////////////
// if numeric remove any non-numerics //
////////////////////////////////////////
let saveData = [];
for (let i = 0; i < chipData.length; i++)
if (type !== "number" || !Number.isNaN(Number(chipData[i])))
{
if (type !== "number" || !Number.isNaN(Number(chipData[i])))
{
saveData.push(chipData[i]);
}
saveData.push(chipData[i]);
}
if (gridFilterItem.value)
{
gridFilterItem.value = [...gridFilterItem.value, ...saveData];
}
else
{
gridFilterItem.value = saveData;
}
setGridFilterItem(gridFilterItem);
props.applyValue(gridFilterItem);
}
*/
onSave(saveData);
clearData();
setPasteModalIsOpen(false);
@ -299,7 +277,7 @@ function FilterCriteriaPaster({type}: Props): JSX.Element
return (
<Box>
<Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source.">
<Icon onClick={handlePasteClick} fontSize="small" color="info" sx={{marginLeft: "10px", cursor: "pointer"}}>paste_content</Icon>
<Icon onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
</Tooltip>
{
pasteModalIsOpen &&

View File

@ -20,6 +20,7 @@
*/
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
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";
@ -57,12 +58,14 @@ export interface OperatorOption
valueMode: ValueMode;
}
export const getDefaultCriteriaValue = () => [""];
interface FilterCriteriaRowProps
{
id: number;
index: number;
tableMetaData: QTableMetaData;
metaData: QInstance;
criteria: QFilterCriteria;
booleanOperator: "AND" | "OR" | null;
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
@ -82,11 +85,11 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a
}
}
export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
{
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
const [operatorInputValue, setOperatorInputValue] = useState("")
const [operatorInputValue, setOperatorInputValue] = useState("");
///////////////////////////////////////////////////////////////
// set up the array of options for the fields Autocomplete //
@ -98,12 +101,14 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
{
fieldsGroupBy = (option: any) => `${option.table.label} Fields`;
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const exposedJoin = tableMetaData.exposedJoins[i];
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
if (metaData.tables.has(exposedJoin.joinTable.name))
{
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
}
}
}
@ -124,8 +129,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
//////////////////////////////////////////////////////
if (field.possibleValueSourceName)
{
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.PVS_SINGLE});
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 any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI});
@ -138,7 +143,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "not equals", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE});
@ -151,8 +156,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
break;
case QFieldType.DATE:
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE});
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 before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
@ -163,8 +168,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
//? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN});
break;
case QFieldType.DATE_TIME:
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
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 before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
@ -175,8 +180,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
break;
case QFieldType.BOOLEAN:
operatorOptions.push({label: "is yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
operatorOptions.push({label: "is no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
operatorOptions.push({label: "equals yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
operatorOptions.push({label: "equals no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
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});
/*
@ -266,20 +271,39 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
//////////////////////////////////////////
const handleFieldChange = (event: any, newValue: any, reason: string) =>
{
criteria.fieldName = newValue ? newValue.fieldName : null;
updateCriteria(criteria, false);
const oldFieldName = criteria.fieldName;
setOperatorOptions(criteria.fieldName)
if(operatorOptions.length)
criteria.fieldName = newValue ? newValue.fieldName : null;
//////////////////////////////////////////////////////
// decide if we should clear out the values or not. //
//////////////////////////////////////////////////////
if (criteria.fieldName == null || isFieldTypeDifferent(oldFieldName, criteria.fieldName))
{
setOperatorSelectedValue(operatorOptions[0]);
setOperatorInputValue(operatorOptions[0].label);
criteria.values = getDefaultCriteriaValue();
}
////////////////////////////////////////////////////////////////////
// update the operator options, and the operator on this criteria //
////////////////////////////////////////////////////////////////////
setOperatorOptions(criteria.fieldName);
if (operatorOptions.length)
{
if (isFieldTypeDifferent(oldFieldName, criteria.fieldName))
{
criteria.operator = operatorOptions[0].value;
setOperatorSelectedValue(operatorOptions[0]);
setOperatorInputValue(operatorOptions[0].label);
}
}
else
{
criteria.operator = null;
setOperatorSelectedValue(null);
setOperatorInputValue("");
}
updateCriteria(criteria, false);
};
/////////////////////////////////////////////
@ -314,7 +338,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
{
// @ts-ignore
const value = newValue ? newValue : event ? event.target.value : null;
const value = newValue !== undefined ? newValue : event ? event.target.value : null;
if(!criteria.values)
{
@ -323,7 +347,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
if(valueIndex == "all")
{
criteria.values= value;
criteria.values = value;
}
else
{
@ -333,6 +357,22 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
updateCriteria(criteria, true);
};
const isFieldTypeDifferent = (fieldNameA: string, fieldNameB: string): boolean =>
{
const [fieldA] = FilterUtils.getField(tableMetaData, fieldNameA);
const [fieldB] = FilterUtils.getField(tableMetaData, fieldNameB);
if (fieldA?.type !== fieldB.type)
{
return (true);
}
if (fieldA.possibleValueSourceName !== fieldB.possibleValueSourceName)
{
return (true);
}
return (false);
};
function isFieldOptionEqual(option: any, value: any)
{
return option.fieldName === value.fieldName;
@ -465,6 +505,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {style: {padding: 0, width: "250px"}}}}
/>
</Box>
<Box display="inline-block" width={200}>
@ -481,6 +522,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp
getOptionLabel={(option: any) => option.label}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {style: {padding: 0, maxHeight: "unset", width: "200px"}}}}
/*disabled={criteria.fieldName == null}*/
/>
</Tooltip>

View File

@ -25,11 +25,16 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
import TextField from "@mui/material/TextField";
import React, {SyntheticEvent} from "react";
import React, {SyntheticEvent, useReducer} from "react";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props
{
@ -45,31 +50,64 @@ FilterCriteriaRowValues.defaultProps = {
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
{
if(!operatorOption)
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!operatorOption)
{
return <br />
return <br />;
}
const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix="value-") =>
const getTypeForTextField = (): string =>
{
let type = "search"
const inputLabelProps: any = {};
let type = "search";
if(field.type == QFieldType.INTEGER)
if (field.type == QFieldType.INTEGER)
{
type = "number";
}
else if(field.type == QFieldType.DATE)
else if (field.type == QFieldType.DATE)
{
type = "date";
inputLabelProps.shrink = true;
}
else if(field.type == QFieldType.DATE_TIME)
else if (field.type == QFieldType.DATE_TIME)
{
type = "datetime-local";
}
return (type);
};
const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
{
let type = getTypeForTextField();
const inputLabelProps: any = {};
if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME)
{
inputLabelProps.shrink = true;
}
let value = criteria.values[valueIndex];
if (field.type == QFieldType.DATE_TIME && value && String(value).indexOf("Z") > -1)
{
value = ValueUtils.formatDateTimeValueForForm(value);
}
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{
valueChangeHandler(event, index, "");
document.getElementById(`${idPrefix}${criteria.id}`).focus();
};
const inputProps: any = {};
inputProps.endAdornment = (
<InputAdornment position="end">
<IconButton sx={{visibility: value ? "visible" : "hidden"}} onClick={(event) => clearValue(event, valueIndex)}>
<Icon>close</Icon>
</IconButton>
</InputAdornment>
);
return <TextField
id={`${idPrefix}${criteria.id}`}
label={label}
@ -77,17 +115,40 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
autoComplete="off"
type={type}
onChange={(event) => valueChangeHandler(event, valueIndex)}
value={criteria.values[valueIndex]}
value={value}
InputLabelProps={inputLabelProps}
InputProps={inputProps}
fullWidth
// todo - x to clear value?
/>
/>;
};
function saveNewPasterValues(newValues: any[])
{
if (criteria.values)
{
criteria.values = [...criteria.values, ...newValues];
}
else
{
criteria.values = newValues;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we are somehow getting some empty-strings as first-value leaking through. they aren't cool, so, remove them if we find them //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (criteria.values.length > 0 && criteria.values[0] == "")
{
criteria.values = criteria.values.splice(1);
}
valueChangeHandler(null, "all", criteria.values);
forceUpdate();
}
switch (operatorOption.valueMode)
{
case ValueMode.NONE:
return <br />
return <br />;
case ValueMode.SINGLE:
return makeTextField();
case ValueMode.SINGLE_DATE:
@ -100,30 +161,36 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
{ makeTextField(0, "From", "from-") }
</Box>
<Box width="50%" display="inline-block">
{ makeTextField(1, "To", "to-") }
{makeTextField(1, "To", "to-")}
</Box>
</Box>;
case ValueMode.MULTI:
let values = criteria.values;
if(values && values.length == 1 && values[0] == "")
if (values && values.length == 1 && values[0] == "")
{
values = [];
}
return <Autocomplete
renderInput={(params) => (<TextField {...params} variant="standard" label="Values" />)}
options={[]}
multiple
freeSolo // todo - no debounce after enter?
selectOnFocus
clearOnBlur
limitTags={5}
value={values}
onChange={(event, value) => valueChangeHandler(event, "all", value)}
/>
// todo - need the Paste button
return <Box display="flex" alignItems="flex-end">
<Autocomplete
renderInput={(params) => (<TextField {...params} variant="standard" label="Values" />)}
options={[]}
multiple
freeSolo // todo - no debounce after enter?
selectOnFocus
clearOnBlur
fullWidth
limitTags={5}
value={values}
onChange={(event, value) => valueChangeHandler(event, "all", value)}
/>
<Box>
<FilterCriteriaPaster type={getTypeForTextField()} onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
</Box>
</Box>;
case ValueMode.PVS_SINGLE:
console.log("Doing pvs single: " + criteria.values);
let selectedPossibleValue = null;
if(criteria.values && criteria.values.length > 0)
if (criteria.values && criteria.values.length > 0)
{
selectedPossibleValue = criteria.values[0];
}
@ -131,22 +198,38 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
<DynamicSelect
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-single-" + criteria.id}
key={field.name + "-single-" + criteria.id}
fieldLabel="Value"
initialValue={selectedPossibleValue?.id}
initialDisplayValue={selectedPossibleValue?.label}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)}
/>
</Box>
</Box>;
case ValueMode.PVS_MULTI:
// todo - values not sticking when re-opening filter panel
console.log("Doing pvs multi: " + criteria.values);
let initialValues: any[] = [];
if (criteria.values && criteria.values.length > 0)
{
if (criteria.values.length == 1 && criteria.values[0] == "")
{
// we never want a tag that's just ""...
}
else
{
initialValues = criteria.values;
}
}
return <Box mb={-1.5}>
<DynamicSelect
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-multi-" + criteria.id}
key={field.name + "-multi-" + criteria.id}
isMultiple
fieldLabel="Values"
initialValues={criteria.values || []}
initialValues={initialValues}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
/>

View File

@ -350,7 +350,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////////////////////////
const buildQFilter = (tableMetaData: QTableMetaData, filterModel: GridFilterModel, limit?: number) =>
{
const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit);
let filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit);
filter = FilterUtils.convertFilterPossibleValuesToIds(filter);
setHasValidFilters(filter.criteria && filter.criteria.length > 0);
return (filter);
};
@ -879,11 +880,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
console.log(columnOrderChangeParams);
};
const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true) =>
const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true, isChangeFromDataGrid = false) =>
{
setFilterModel(filterModel);
if(doSetQueryFilter)
if (doSetQueryFilter)
{
//////////////////////////////////////////////////////////////////////////////////
// someone might have already set the query filter, so, only set it if asked to //
@ -891,6 +892,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage));
}
if (isChangeFromDataGrid)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this function is called by our code several times, but also from dataGridPro when its filter model changes. //
// in general, we don't want a "partial" criteria to be part of our query filter object (e.g., w/ no values) //
// BUT - for one use-case, when the user adds a "filter" (criteria) from column-header "..." menu, then dataGridPro //
// puts a partial item in its filter - so - in that case, we do like to get this partial criteria in our QFilter. //
// so far, not seeing any negatives to this being here, and it fixes that user experience, so keep this. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage, true));
}
if (filterLocalStorageKey)
{
localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel));
@ -1700,7 +1713,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// to avoid both this useEffect and the one below from both doing an "initial query", //
// only run this one if at least 1 query has already been ran //
////////////////////////////////////////////////////////////////////////////////////////
// console.log("calling update table for UE 1");
updateTable();
}
}, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]);
@ -1712,7 +1724,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
setTotalRecords(null);
setDistinctRecords(null);
// console.log("calling update table for UE 2");
updateTable();
}, [columnsModel, tableState]);
@ -1722,19 +1733,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
currentQFilter.skip = pageNumber * rowsPerPage;
const currentQFilterJSON = JSON.stringify(currentQFilter);
// console.log(`current ${currentQFilterJSON}`);
// console.log(`last... ${lastFetchedQFilterJSON}`);
if(currentQFilterJSON !== lastFetchedQFilterJSON)
{
setTotalRecords(null);
setDistinctRecords(null);
// console.log("calling update table for UE 3");
updateTable();
}
else
{
// console.log("NOT calling update table for UE 3!!");
}
}, [filterModel]);
@ -1744,12 +1748,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
document.scrollingElement.scrollTop = 0;
}, [pageNumber, rowsPerPage]);
const updateFilter = (newFilter: QQueryFilter): void =>
const updateFilterFromFilterPanel = (newFilter: QQueryFilter): void =>
{
setQueryFilter(newFilter);
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
handleFilterChange(gridFilterModel, false);
}
};
if (tableMetaData && !tableMetaData.readPermission)
{
@ -1813,7 +1817,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}
<Box display="flex" justifyContent="flex-end" alignItems="flex-start" mb={2}>
<Box display="flex" marginRight="auto">
<SavedFilters qController={qController} metaData={metaData} tableMetaData={tableMetaData} currentSavedFilter={currentSavedFilter} filterModel={filterModel} columnSortModel={columnSortModel} filterOnChangeCallback={handleSavedFilterChange} />
{
metaData && metaData.processes.has("querySavedFilter") &&
<SavedFilters qController={qController} metaData={metaData} tableMetaData={tableMetaData} currentSavedFilter={currentSavedFilter} filterModel={filterModel} columnSortModel={columnSortModel} filterOnChangeCallback={handleSavedFilterChange} />
}
</Box>
<Box display="flex" width="150px">
@ -1840,6 +1847,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
columnsPanel:
{
tableMetaData: tableMetaData,
metaData: metaData,
initialOpenedGroups: columnChooserOpenGroups,
openGroupsChanger: setColumnChooserOpenGroups,
initialFilterText: columnChooserFilterText,
@ -1848,8 +1856,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
filterPanel:
{
tableMetaData: tableMetaData,
metaData: metaData,
queryFilter: queryFilter,
updateFilter: updateFilter
updateFilter: updateFilterFromFilterPanel
}
}}
localeText={{
@ -1880,7 +1889,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
density={density}
loading={loading}
filterModel={filterModel}
onFilterModelChange={(model) => handleFilterChange(model)}
onFilterModelChange={(model) => handleFilterChange(model, true, true)}
columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onColumnOrderChange={handleColumnOrderChange}

View File

@ -421,42 +421,50 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
top: -60px !important;
}
/* tighten the text in the field select dropdown in custom filters */
.customFilterPanel .MuiAutocomplete-paper
{
line-height: 1.375;
}
/* tighten the text in the field select dropdown in custom filters */
.customFilterPanel .MuiAutocomplete-groupLabel
{
line-height: 1.75;
}
/* taller list box */
.customFilterPanel .MuiAutocomplete-listbox
{
max-height: 60vh;
}
/* shrink down-arrows in custom filters panel */
.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard,
.customFilterPanel .MuiSvgIcon-root
{
font-size: 14px !important;
}
/* fix something in AND/OR dropdown in filters */
.customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root
{
display: inline-block !important;
}
/* adjust bottom of AND/OR dropdown in filters */
.customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl
{
padding-bottom: calc(0.25rem + 1px);
}
/* adjust down-arrow in AND/OR dropdown in filters */
.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard
{
top: calc(50% - 0.75rem);
}
/* change tags in any-of value fields to not be black bg with white text */
.customFilterPanel .filterValuesColumn .MuiChip-root
{
background: none;
@ -464,13 +472,32 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
border: 1px solid gray;
}
/* change 'x' icon in tags in any-of value */
.customFilterPanel .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
{
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
{
color: lightgray;
}
.DynamicSelectPopper ul
{
padding: 0;
}
.DynamicSelectPopper ul li.MuiAutocomplete-option
{
padding-left: 0.25rem;
padding-right: 0.25rem;
}

View File

@ -25,13 +25,42 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {getGridDateOperators, GridColDef, GridRowsProp} from "@mui/x-data-grid-pro";
import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import React from "react";
import {Link} from "react-router-dom";
import {buildQGridPvsOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
const emptyApplyFilterFn = (filterItem: GridFilterItem, column: GridColDef): null => null;
function NullInputComponent()
{
return (<React.Fragment />);
}
const makeGridFilterOperator = (value: string, label: string, takesValues: boolean = false): GridFilterOperator =>
{
const rs: GridFilterOperator = {value: value, label: label, getApplyFilterFn: emptyApplyFilterFn};
if (takesValues)
{
rs.InputComponent = NullInputComponent;
}
return (rs);
};
const QGridDateOperators = [
makeGridFilterOperator("equals", "equals", true),
makeGridFilterOperator("isNot", "not equals", 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"),
];
export default class DataGridUtils
{
@ -40,7 +69,7 @@ export default class DataGridUtils
*******************************************************************************/
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData): GridRowsProp[] =>
{
const fields = [ ...tableMetaData.fields.values() ];
const fields = [...tableMetaData.fields.values()];
const rows = [] as any[];
let rowIndex = 0;
results.forEach((record: QRecord) =>
@ -188,6 +217,7 @@ export default class DataGridUtils
});
}
/*******************************************************************************
**
*******************************************************************************/
@ -220,12 +250,12 @@ export default class DataGridUtils
case QFieldType.DATE:
columnType = "date";
columnWidth = 100;
filterOperators = getGridDateOperators();
filterOperators = QGridDateOperators;
break;
case QFieldType.DATE_TIME:
columnType = "dateTime";
columnWidth = 200;
filterOperators = getGridDateOperators(true);
filterOperators = QGridDateOperators;
break;
case QFieldType.BOOLEAN:
columnType = "string"; // using boolean gives an odd 'no' for nulls.

View File

@ -264,10 +264,10 @@ class FilterUtils
/////////////////////////////////////////////////////////////////////////////////////////////////
return ([null, null]);
}
return (FilterUtils.prepFilterValuesForBackend(value, fieldMetaData));
return (FilterUtils.cleanseCriteriaValueForQQQ(value, fieldMetaData));
}
return (FilterUtils.prepFilterValuesForBackend([value], fieldMetaData));
return (FilterUtils.cleanseCriteriaValueForQQQ([value], fieldMetaData));
};
@ -278,7 +278,7 @@ class FilterUtils
**
** Or, if the values are date-times, convert them to UTC.
*******************************************************************************/
private static prepFilterValuesForBackend = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] =>
private static cleanseCriteriaValueForQQQ = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] =>
{
if (param === null || param === undefined)
{
@ -291,10 +291,15 @@ class FilterUtils
console.log(param[i]);
if (param[i] && param[i].id && param[i].label)
{
/////////////////////////////////////////////////////////////
// if the param looks like a possible value, return its id //
/////////////////////////////////////////////////////////////
rs.push(param[i].id);
//////////////////////////////////////////////////////////////////////////////////////////
// if the param looks like a possible value, return its id //
// during build of new custom filter panel, this ended up causing us //
// problems (because we wanted the full PV object in the filter model for the frontend) //
// so, we can keep the PV as-is here, and see calls to convertFilterPossibleValuesToIds //
// to do what this used to do. //
//////////////////////////////////////////////////////////////////////////////////////////
// rs.push(param[i].id);
rs.push(param[i]);
}
else
{
@ -464,7 +469,16 @@ class FilterUtils
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)];
}
}
@ -598,7 +612,7 @@ class FilterUtils
/*******************************************************************************
** build a qqq filter from a grid and column sort model
*******************************************************************************/
public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[], limit?: number): QQueryFilter
public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[], limit?: number, allowIncompleteCriteria = false): QQueryFilter
{
console.log("Building q filter with model:");
console.log(filterModel);
@ -638,13 +652,15 @@ class FilterUtils
////////////////////////////////////////////////////////////////////////////////
// if no value set and not 'empty' or 'not empty' operators, skip this filter //
////////////////////////////////////////////////////////////////////////////////
if ((!item.value || item.value.length == 0) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
if ((!item.value || item.value.length == 0 || (item.value.length == 1 && item.value[0] == "")) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
{
return;
if (!allowIncompleteCriteria)
{
return;
}
}
var fieldMetadata = tableMetaData?.fields.get(item.columnField);
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));
@ -664,6 +680,33 @@ class FilterUtils
return qFilter;
};
public static convertFilterPossibleValuesToIds(inputFilter: QQueryFilter): QQueryFilter
{
const filter = Object.assign({}, inputFilter);
if (filter.criteria)
{
for (let i = 0; i < filter.criteria.length; i++)
{
const criteria = filter.criteria[i];
if (criteria.values)
{
for (let j = 0; j < criteria.values.length; j++)
{
let value = criteria.values[j];
if (value && value.id && value.label)
{
criteria.values[j] = value.id;
}
}
}
}
}
return (filter);
}
}
export default FilterUtils;