mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
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:
@ -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
|
||||
}
|
||||
]);
|
||||
};
|
@ -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) => (
|
||||
|
Reference in New Issue
Block a user