CE-1955 Remove filterOperators from the column objects we produce, since we're not using dataGridPro's filtering any more

This commit is contained in:
2024-12-03 09:11:59 -06:00
parent 65b347b794
commit 6db003026b
2 changed files with 2 additions and 1002 deletions

View File

@ -1,945 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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 {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {FormControl, InputLabel, Select, SelectChangeEvent, TextFieldProps} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import {getGridNumericOperators, getGridStringOperators, GridColDef, GridFilterInputMultipleValue, GridFilterInputMultipleValueProps, GridFilterInputValueProps, GridFilterItem} from "@mui/x-data-grid-pro";
import {GridFilterInputValue} from "@mui/x-data-grid/components/panel/filterPanel/GridFilterInputValue";
import {GridApiCommunity} from "@mui/x-data-grid/internals";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import React, {useEffect, useRef, useState} from "react";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import ChipTextField from "qqq/components/forms/ChipTextField";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
////////////////////////////////
// input element for 'is any' //
////////////////////////////////
function CustomIsAnyInput(type: "number" | "text", props: GridFilterInputValueProps)
{
enum Delimiter
{
DETECT_AUTOMATICALLY = "Detect Automatically",
COMMA = "Comma",
NEWLINE = "Newline",
PIPE = "Pipe",
SPACE = "Space",
TAB = "Tab",
CUSTOM = "Custom",
}
const delimiterToCharacterMap: { [key: string]: string } = {};
delimiterToCharacterMap[Delimiter.COMMA] = "[,\n\r]";
delimiterToCharacterMap[Delimiter.TAB] = "[\t,\n,\r]";
delimiterToCharacterMap[Delimiter.NEWLINE] = "[\n\r]";
delimiterToCharacterMap[Delimiter.PIPE] = "[\\|\r\n]";
delimiterToCharacterMap[Delimiter.SPACE] = "[ \n\r]";
const delimiterDropdownOptions = Object.values(Delimiter);
const mainCardStyles: any = {};
mainCardStyles.width = "60%";
mainCardStyles.minWidth = "500px";
const [gridFilterItem, setGridFilterItem] = useState(props.item);
const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false);
const [inputText, setInputText] = useState("");
const [delimiter, setDelimiter] = useState("");
const [delimiterCharacter, setDelimiterCharacter] = useState("");
const [customDelimiterValue, setCustomDelimiterValue] = useState("");
const [chipData, setChipData] = useState(undefined);
const [detectedText, setDetectedText] = useState("");
const [errorText, setErrorText] = useState("");
//////////////////////////////////////////////////////////////
// handler for when paste icon is clicked in 'any' operator //
//////////////////////////////////////////////////////////////
const handlePasteClick = (event: any) =>
{
event.target.blur();
setPasteModalIsOpen(true);
};
const applyValue = (item: GridFilterItem) =>
{
console.log(`updating grid values: ${JSON.stringify(item.value)}`);
setGridFilterItem(item);
props.applyValue(item);
};
const clearData = () =>
{
setDelimiter("");
setDelimiterCharacter("");
setChipData([]);
setInputText("");
setDetectedText("");
setCustomDelimiterValue("");
setPasteModalIsOpen(false);
};
const handleCancelClicked = () =>
{
clearData();
setPasteModalIsOpen(false);
};
const handleSaveClicked = () =>
{
if (gridFilterItem)
{
////////////////////////////////////////
// if numeric remove any non-numerics //
////////////////////////////////////////
let saveData = [];
for (let i = 0; i < chipData.length; i++)
{
if (type !== "number" || !Number.isNaN(Number(chipData[i])))
{
saveData.push(chipData[i]);
}
}
if (gridFilterItem.value)
{
gridFilterItem.value = [...gridFilterItem.value, ...saveData];
}
else
{
gridFilterItem.value = saveData;
}
setGridFilterItem(gridFilterItem);
props.applyValue(gridFilterItem);
}
clearData();
setPasteModalIsOpen(false);
};
////////////////////////////////////////////////////////////////
// when user selects a different delimiter on the parse modal //
////////////////////////////////////////////////////////////////
const handleDelimiterChange = (event: SelectChangeEvent) =>
{
const newDelimiter = event.target.value;
console.log(`Delimiter Changed to ${JSON.stringify(newDelimiter)}`);
setDelimiter(newDelimiter);
if (newDelimiter === Delimiter.CUSTOM)
{
setDelimiterCharacter(customDelimiterValue);
}
else
{
setDelimiterCharacter(delimiterToCharacterMap[newDelimiter]);
}
};
const handleTextChange = (event: any) =>
{
const inputText = event.target.value;
setInputText(inputText);
};
const handleCustomDelimiterChange = (event: any) =>
{
let inputText = event.target.value;
setCustomDelimiterValue(inputText);
};
///////////////////////////////////////////////////////////////////////////////////////
// iterate over each character, putting them into 'buckets' so that we can determine //
// a good default to use when data is pasted into the textarea //
///////////////////////////////////////////////////////////////////////////////////////
const calculateAutomaticDelimiter = (text: string): string =>
{
const buckets = new Map();
for (let i = 0; i < text.length; i++)
{
let bucketName = "";
switch (text.charAt(i))
{
case "\t":
bucketName = Delimiter.TAB;
break;
case "\n":
case "\r":
bucketName = Delimiter.NEWLINE;
break;
case "|":
bucketName = Delimiter.PIPE;
break;
case " ":
bucketName = Delimiter.SPACE;
break;
case ",":
bucketName = Delimiter.COMMA;
break;
}
if (bucketName !== "")
{
let currentCount = (buckets.has(bucketName)) ? buckets.get(bucketName) : 0;
buckets.set(bucketName, currentCount + 1);
}
}
///////////////////////
// default is commas //
///////////////////////
let highestCount = 0;
let delimiter = Delimiter.COMMA;
for (let j = 0; j < delimiterDropdownOptions.length; j++)
{
let bucketName = delimiterDropdownOptions[j];
if (buckets.has(bucketName) && buckets.get(bucketName) > highestCount)
{
delimiter = bucketName;
highestCount = buckets.get(bucketName);
}
}
setDetectedText(`${delimiter} Detected`);
return (delimiterToCharacterMap[delimiter]);
};
useEffect(() =>
{
let currentDelimiter = delimiter;
let currentDelimiterCharacter = delimiterCharacter;
/////////////////////////////////////////////////////////////////////////////
// if no delimiter already set in the state, call function to determine it //
/////////////////////////////////////////////////////////////////////////////
if (!currentDelimiter || currentDelimiter === Delimiter.DETECT_AUTOMATICALLY)
{
currentDelimiterCharacter = calculateAutomaticDelimiter(inputText);
if (!currentDelimiterCharacter)
{
return;
}
currentDelimiter = Delimiter.DETECT_AUTOMATICALLY;
setDelimiter(Delimiter.DETECT_AUTOMATICALLY);
setDelimiterCharacter(currentDelimiterCharacter);
}
else if (currentDelimiter === Delimiter.CUSTOM)
{
////////////////////////////////////////////////////
// if custom, make sure to split on new lines too //
////////////////////////////////////////////////////
currentDelimiterCharacter = `[${customDelimiterValue}\r\n]`;
}
console.log(`current delimiter is: ${currentDelimiter}, delimiting on: ${currentDelimiterCharacter}`);
let regex = new RegExp(currentDelimiterCharacter);
let parts = inputText.split(regex);
let chipData = [] as string[];
///////////////////////////////////////////////////////
// if delimiter is empty string, dont split anything //
///////////////////////////////////////////////////////
setErrorText("");
if (currentDelimiterCharacter !== "")
{
for (let i = 0; i < parts.length; i++)
{
let part = parts[i].trim();
if (part !== "")
{
chipData.push(part);
///////////////////////////////////////////////////////////
// if numeric, check that first before pushing as a chip //
///////////////////////////////////////////////////////////
if (type === "number" && Number.isNaN(Number(part)))
{
setErrorText("Some values are not numbers");
}
}
}
}
setChipData(chipData);
}, [inputText, delimiterCharacter, customDelimiterValue, detectedText]);
return (
<Box>
{
props &&
(
<Box id="testId" sx={{width: "100%", display: "inline-flex", flexDirection: "row", alignItems: "end", height: 48}}>
<GridFilterInputMultipleValue
sx={{width: "100%"}}
variant="standard"
type={type} {...props}
applyValue={applyValue}
item={gridFilterItem}
/>
<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>
</Tooltip>
</Box>
)
}
{
pasteModalIsOpen &&
(
<Modal open={pasteModalIsOpen}>
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
<Card sx={mainCardStyles}>
<Box p={4} pb={2}>
<Grid container>
<Grid item pr={3} xs={12} lg={12}>
<Typography variant="h5">Bulk Add Filter Values</Typography>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
Paste into the box on the left.
Review the filter values in the box on the right.
If the filter values are not what are expected, try changing the separator using the dropdown below.
</Typography>
</Grid>
</Grid>
</Box>
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<FormControl sx={{m: 1, width: "100%"}}>
<TextField
id="outlined-multiline-static"
label="PASTE TEXT"
multiline
onChange={handleTextChange}
rows={16}
value={inputText}
/>
</FormControl>
</Grid>
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
<FormControl sx={{m: 1, width: "100%"}}>
<ChipTextField
handleChipChange={() =>
{
}}
chipData={chipData}
chipType={type}
multiline
fullWidth
variant="outlined"
id="tags"
rows={0}
name="tags"
label="FILTER VALUES REVIEW"
/>
</FormControl>
</Grid>
</Grid>
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
<Grid item pl={1} pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<Box sx={{display: "inline-flex", alignItems: "baseline"}}>
<FormControl sx={{mt: 2, width: "50%"}}>
<InputLabel htmlFor="select-native">
SEPARATOR
</InputLabel>
<Select
multiline
native
value={delimiter}
onChange={handleDelimiterChange}
label="SEPARATOR"
size="medium"
inputProps={{
id: "select-native",
}}
>
{delimiterDropdownOptions.map((delimiter) => (
<option key={delimiter} value={delimiter}>
{delimiter}
</option>
))}
</Select>
</FormControl>
{delimiter === Delimiter.CUSTOM.valueOf() && (
<FormControl sx={{pl: 2, top: 5, width: "50%"}}>
<TextField
name="custom-delimiter-value"
placeholder="Custom Separator"
label="Custom Separator"
variant="standard"
value={customDelimiterValue}
onChange={handleCustomDelimiterChange}
inputProps={{maxLength: 1}}
/>
</FormControl>
)}
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}>
<i>{detectedText}</i>
</Typography>
)}
</Box>
</Grid>
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}>
{
errorText && chipData.length > 0 && (
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
<Icon color="error">error</Icon>
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">{errorText}</Typography>
</Box>
)
}
</Grid>
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}>
{
chipData && chipData.length > 0 && (
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}</Typography>
)
}
</Grid>
</Grid>
<Box p={3} pt={0}>
<Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
<QCancelButton
onClickHandler={handleCancelClicked}
iconName="cancel"
disabled={false} />
<QSaveButton onClickHandler={handleSaveClicked} label="Add Filters" disabled={false} />
</Grid>
</Box>
</Card>
</Box>
</Box>
</Modal>
)
}
</Box>
);
}
//////////////////////
// string operators //
//////////////////////
const stringNotEqualsOperator: GridFilterOperator = {
label: "does not equal",
value: "isNot",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: GridFilterInputValue,
};
const stringNotContainsOperator: GridFilterOperator = {
label: "does not contain",
value: "notContains",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: GridFilterInputValue,
};
const stringNotStartsWithOperator: GridFilterOperator = {
label: "does not start with",
value: "notStartsWith",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: GridFilterInputValue,
};
const stringNotEndWithOperator: GridFilterOperator = {
label: "does not end with",
value: "notEndsWith",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: GridFilterInputValue,
};
const getListValueString = (value: GridFilterItem["value"]): string =>
{
if (value && value.length)
{
let labels = [] as string[];
let maxLoops = value.length;
if(maxLoops > 5)
{
maxLoops = 3;
}
for (let i = 0; i < maxLoops; i++)
{
labels.push(value[i]);
}
if(maxLoops < value.length)
{
labels.push(" and " + (value.length - maxLoops) + " other values.");
}
return (labels.join(", "));
}
return (value);
};
const stringIsAnyOfOperator: GridFilterOperator = {
label: "is any of",
value: "isAnyOf",
getValueAsString: getListValueString,
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("text", props)
};
const stringIsNoneOfOperator: GridFilterOperator = {
label: "is none of",
value: "isNone",
getValueAsString: getListValueString,
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("text", props)
};
let gridStringOperators = getGridStringOperators();
let equals = gridStringOperators.splice(1, 1)[0];
let contains = gridStringOperators.splice(0, 1)[0];
let startsWith = gridStringOperators.splice(0, 1)[0];
let endsWith = gridStringOperators.splice(0, 1)[0];
///////////////////////////////////
// remove default isany operator //
///////////////////////////////////
gridStringOperators.splice(2, 1)[0];
gridStringOperators = [equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators, stringIsAnyOfOperator, stringIsNoneOfOperator];
export const QGridStringOperators = gridStringOperators;
///////////////////////////////////////
// input element for numbers-between //
///////////////////////////////////////
function InputNumberInterval(props: GridFilterInputValueProps)
{
const SUBMIT_FILTER_STROKE_TIME = 500;
const {item, applyValue, focusElementRef = null} = props;
const filterTimeout = useRef<any>();
const [filterValueState, setFilterValueState] = useState<[string, string]>(
item.value ?? "",
);
const [applying, setIsApplying] = useState(false);
useEffect(() =>
{
return () =>
{
clearTimeout(filterTimeout.current);
};
}, []);
useEffect(() =>
{
const itemValue = item.value ?? [undefined, undefined];
setFilterValueState(itemValue);
}, [item.value]);
const updateFilterValue = (lowerBound: string, upperBound: string) =>
{
clearTimeout(filterTimeout.current);
setFilterValueState([lowerBound, upperBound]);
setIsApplying(true);
filterTimeout.current = setTimeout(() =>
{
setIsApplying(false);
applyValue({...item, value: [lowerBound, upperBound]});
}, SUBMIT_FILTER_STROKE_TIME);
};
const handleUpperFilterChange: TextFieldProps["onChange"] = (event) =>
{
const newUpperBound = event.target.value;
updateFilterValue(filterValueState[0], newUpperBound);
};
const handleLowerFilterChange: TextFieldProps["onChange"] = (event) =>
{
const newLowerBound = event.target.value;
updateFilterValue(newLowerBound, filterValueState[1]);
};
return (
<Box
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "end",
height: 48,
pl: "20px",
}}
>
<TextField
name="lower-bound-input"
placeholder="From"
label="From"
variant="standard"
value={Number(filterValueState[0])}
onChange={handleLowerFilterChange}
type="number"
inputRef={focusElementRef}
sx={{mr: 2}}
/>
<TextField
name="upper-bound-input"
placeholder="To"
label="To"
variant="standard"
value={Number(filterValueState[1])}
onChange={handleUpperFilterChange}
type="number"
InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
/>
</Box>
);
}
//////////////////////
// number operators //
//////////////////////
const betweenOperator: GridFilterOperator = {
label: "is between",
value: "between",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: InputNumberInterval
};
const notBetweenOperator: GridFilterOperator = {
label: "is not between",
value: "notBetween",
getApplyFilterFn: () => null,
// @ts-ignore
InputComponent: InputNumberInterval
};
const numericIsAnyOfOperator: GridFilterOperator = {
label: "is any of",
value: "isAnyOf",
getApplyFilterFn: () => null,
getValueAsString: getListValueString,
// @ts-ignore
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("number", props)
};
const numericIsNoneOfOperator: GridFilterOperator = {
label: "is none of",
value: "isNone",
getApplyFilterFn: () => null,
getValueAsString: getListValueString,
// @ts-ignore
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("number", props)
};
//////////////////////////////
// remove default is any of //
//////////////////////////////
let gridNumericOperators = getGridNumericOperators();
gridNumericOperators.splice(8, 1)[0];
export const QGridNumericOperators = [...gridNumericOperators, betweenOperator, notBetweenOperator, numericIsAnyOfOperator, numericIsNoneOfOperator];
///////////////////////
// boolean operators //
///////////////////////
const booleanTrueOperator: GridFilterOperator = {
label: "is yes",
value: "isTrue",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
const booleanFalseOperator: GridFilterOperator = {
label: "is no",
value: "isFalse",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
const booleanEmptyOperator: GridFilterOperator = {
label: "is empty",
value: "isEmpty",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
const booleanNotEmptyOperator: GridFilterOperator = {
label: "is not empty",
value: "isNotEmpty",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
export const QGridBooleanOperators = [booleanTrueOperator, booleanFalseOperator, booleanEmptyOperator, booleanNotEmptyOperator];
const blobEmptyOperator: GridFilterOperator = {
label: "is empty",
value: "isEmpty",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
const blobNotEmptyOperator: GridFilterOperator = {
label: "is not empty",
value: "isNotEmpty",
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
};
export const QGridBlobOperators = [blobNotEmptyOperator, blobEmptyOperator];
///////////////////////////////////////
// input element for possible values //
///////////////////////////////////////
function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData, props: GridFilterInputValueProps)
{
const SUBMIT_FILTER_STROKE_TIME = 500;
const {item, applyValue, focusElementRef = null} = props;
console.log("Item.value? " + item.value);
const filterTimeout = useRef<any>();
const [filterValueState, setFilterValueState] = useState<any>(item.value ?? null);
const [selectedPossibleValue, setSelectedPossibleValue] = useState((item.value ?? null) as QPossibleValue);
const [applying, setIsApplying] = useState(false);
useEffect(() =>
{
return () =>
{
clearTimeout(filterTimeout.current);
};
}, []);
useEffect(() =>
{
const itemValue = item.value ?? null;
setFilterValueState(itemValue);
}, [item.value]);
const updateFilterValue = (value: QPossibleValue) =>
{
clearTimeout(filterTimeout.current);
setFilterValueState(value);
setIsApplying(true);
filterTimeout.current = setTimeout(() =>
{
setIsApplying(false);
applyValue({...item, value: value});
}, SUBMIT_FILTER_STROKE_TIME);
};
const handleChange = (value: QPossibleValue) =>
{
updateFilterValue(value);
};
return (
<Box
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "end",
height: 48,
}}
>
<DynamicSelect
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
fieldLabel="Value"
initialValue={selectedPossibleValue?.id}
inForm={false}
onChange={handleChange}
useCase="filter"
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
/>
</Box>
);
}
////////////////////////////////////////////////
// input element for multiple possible values //
////////////////////////////////////////////////
function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaData, props: GridFilterInputValueProps)
{
const SUBMIT_FILTER_STROKE_TIME = 500;
const {item, applyValue, focusElementRef = null} = props;
console.log("Item.value? " + item.value);
const filterTimeout = useRef<any>();
const [selectedPossibleValues, setSelectedPossibleValues] = useState(item.value as QPossibleValue[]);
const [applying, setIsApplying] = useState(false);
useEffect(() =>
{
return () =>
{
clearTimeout(filterTimeout.current);
};
}, []);
useEffect(() =>
{
const itemValue = item.value ?? null;
}, [item.value]);
const updateFilterValue = (value: QPossibleValue) =>
{
clearTimeout(filterTimeout.current);
setIsApplying(true);
filterTimeout.current = setTimeout(() =>
{
setIsApplying(false);
applyValue({...item, value: value});
}, SUBMIT_FILTER_STROKE_TIME);
};
const handleChange = (value: QPossibleValue) =>
{
updateFilterValue(value);
};
return (
<Box
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "end",
height: 48,
}}
>
<DynamicSelect
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: null}}
isMultiple={true}
fieldLabel="Value"
inForm={false}
onChange={handleChange}
useCase="filter"
/>
</Box>
);
}
const getPvsValueString = (value: GridFilterItem["value"]): string =>
{
if (value && value.length)
{
let labels = [] as string[];
let maxLoops = value.length;
if(maxLoops > 5)
{
maxLoops = 3;
}
for (let i = 0; i < maxLoops; i++)
{
if(value[i] && value[i].label)
{
labels.push(value[i].label);
}
else
{
labels.push(value);
}
}
if(maxLoops < value.length)
{
labels.push(" and " + (value.length - maxLoops) + " other values.");
}
return (labels.join(", "));
}
else if (value && value.label)
{
return (value.label);
}
return (value);
};
//////////////////////////////////
// possible value set operators //
//////////////////////////////////
export const buildQGridPvsOperators = (tableName: string, field: QFieldMetaData): GridFilterOperator[] =>
{
return ([
{
label: "is",
value: "is",
getApplyFilterFn: () => null,
getValueAsString: getPvsValueString,
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceSingle(tableName, field, props)
},
{
label: "is not",
value: "isNot",
getApplyFilterFn: () => null,
getValueAsString: getPvsValueString,
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceSingle(tableName, field, props)
},
{
label: "is any of",
value: "isAnyOf",
getValueAsString: getPvsValueString,
getApplyFilterFn: () => null,
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceMultiple(tableName, field, props)
},
{
label: "is none of",
value: "isNone",
getValueAsString: getPvsValueString,
getApplyFilterFn: () => null,
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceMultiple(tableName, field, props)
},
{
label: "is empty",
value: "isEmpty",
getValueAsString: getPvsValueString,
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
},
{
label: "is not empty",
value: "isNotEmpty",
getValueAsString: getPvsValueString,
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
}
]);
};

View File

@ -26,62 +26,14 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {GridColDef, GridFilterItem, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React from "react";
import {Link, NavigateFunction} from "react-router-dom";
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);
};
////////////////////////////////////////////////////////////////////////////////////////
// at this point, these may only be used to drive the toolitp on the FILTER button... //
////////////////////////////////////////////////////////////////////////////////////////
const QGridDateOperators = [
makeGridFilterOperator("equals", "equals", true),
makeGridFilterOperator("isNot", "does not equal", true),
makeGridFilterOperator("after", "is after", true),
makeGridFilterOperator("onOrAfter", "is on or after", true),
makeGridFilterOperator("before", "is before", true),
makeGridFilterOperator("onOrBefore", "is on or before", true),
makeGridFilterOperator("isEmpty", "is empty"),
makeGridFilterOperator("isNotEmpty", "is not empty"),
makeGridFilterOperator("between", "is between", true),
makeGridFilterOperator("notBetween", "is not between", true),
];
const QGridDateTimeOperators = [
makeGridFilterOperator("equals", "equals", true),
makeGridFilterOperator("isNot", "does not equal", true),
makeGridFilterOperator("after", "is after", true),
makeGridFilterOperator("onOrAfter", "is at or after", true),
makeGridFilterOperator("before", "is before", true),
makeGridFilterOperator("onOrBefore", "is at or before", true),
makeGridFilterOperator("isEmpty", "is empty"),
makeGridFilterOperator("isNotEmpty", "is not empty"),
makeGridFilterOperator("between", "is between", true),
makeGridFilterOperator("notBetween", "is not between", true),
];
export default class DataGridUtils
{
/*******************************************************************************
@ -295,11 +247,10 @@ export default class DataGridUtils
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, namePrefix?: string, labelPrefix?: string): GridColDef =>
{
let columnType = "string";
let filterOperators: GridFilterOperator<any>[] = QGridStringOperators;
if (field.possibleValueSourceName)
{
filterOperators = buildQGridPvsOperators(tableMetaData.name, field);
// noop here
}
else
{
@ -308,22 +259,17 @@ export default class DataGridUtils
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
columnType = "number";
filterOperators = QGridNumericOperators;
break;
case QFieldType.DATE:
columnType = "date";
filterOperators = QGridDateOperators;
break;
case QFieldType.DATE_TIME:
columnType = "dateTime";
filterOperators = QGridDateTimeOperators;
break;
case QFieldType.BOOLEAN:
columnType = "string"; // using boolean gives an odd 'no' for nulls.
filterOperators = QGridBooleanOperators;
break;
case QFieldType.BLOB:
filterOperators = QGridBlobOperators;
break;
default:
// noop - leave as string
@ -339,7 +285,6 @@ export default class DataGridUtils
headerName: headerName,
width: DataGridUtils.getColumnWidthForField(field, tableMetaData),
renderCell: null as any,
filterOperators: filterOperators,
};
column.renderCell = (cellValues: any) => (