mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
SPRINT-13: checkpoint on front end updates including, bulk add filters, bug when empty value on filter is saved, density storage
This commit is contained in:
1599
package-lock.json
generated
1599
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -30,12 +30,14 @@
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "16.11.21",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react-router-hash-link": "2.4.5",
|
||||
"chart.js": "3.4.1",
|
||||
"chroma-js": "2.4.2",
|
||||
"datejs": "1.0.0-rc3",
|
||||
"downshift": "3.2.10",
|
||||
"dropzone": "5.9.2",
|
||||
"flatpickr": "4.6.9",
|
||||
"form-data": "4.0.0",
|
||||
|
@ -44,15 +44,20 @@ export function QCreateNewButton(): JSX.Element
|
||||
|
||||
interface QSaveButtonProps
|
||||
{
|
||||
label?: string;
|
||||
onClickHandler?: any,
|
||||
disabled: boolean
|
||||
}
|
||||
QSaveButton.defaultProps = {
|
||||
label: "Save"
|
||||
};
|
||||
|
||||
export function QSaveButton({disabled}: QSaveButtonProps): JSX.Element
|
||||
export function QSaveButton({label, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<MDBox ml={3} width={standardWidth}>
|
||||
<MDButton type="submit" variant="gradient" color="info" size="small" fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}>
|
||||
Save
|
||||
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
</MDBox>
|
||||
);
|
||||
|
169
src/qqq/pages/entity-list/ChipTextField.tsx
Normal file
169
src/qqq/pages/entity-list/ChipTextField.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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 {Chip} from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {makeStyles} from "@mui/styles";
|
||||
import Downshift from "downshift";
|
||||
import {arrayOf, func, string} from "prop-types";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
const useStyles = makeStyles((theme: any) => ({
|
||||
chip: {
|
||||
margin: theme.spacing(0.5, 0.25)
|
||||
}
|
||||
}));
|
||||
|
||||
function ChipTextField({...props})
|
||||
{
|
||||
const classes = useStyles();
|
||||
const {handleChipChange, label, chipType, disabled, placeholder, chipData, multiline, rows, ...other} = props;
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [chips, setChips] = useState([]);
|
||||
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setChips(chipData);
|
||||
}, [chipData]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
handleChipChange(chips);
|
||||
}, [chips, handleChipChange]);
|
||||
|
||||
function handleKeyDown(event: any)
|
||||
{
|
||||
if (event.key === "Enter")
|
||||
{
|
||||
const newChipList = [...chips];
|
||||
const duplicatedValues = newChipList.indexOf(
|
||||
event.target.value.trim()
|
||||
);
|
||||
|
||||
if (duplicatedValues !== -1)
|
||||
{
|
||||
setInputValue("");
|
||||
return;
|
||||
}
|
||||
if (!event.target.value.replace(/\s/g, "").length) return;
|
||||
|
||||
newChipList.push(event.target.value.trim());
|
||||
setChips(newChipList);
|
||||
setInputValue("");
|
||||
}
|
||||
else if (chips.length && !inputValue.length && event.key === "Backspace" )
|
||||
{
|
||||
setChips(chips.slice(0, chips.length - 1));
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(item: any)
|
||||
{
|
||||
let newChipList = [...chips];
|
||||
if (newChipList.indexOf(item) === -1)
|
||||
{
|
||||
newChipList = [...newChipList, item];
|
||||
}
|
||||
setInputValue("");
|
||||
setChips(newChipList);
|
||||
}
|
||||
|
||||
const handleDelete = (item: any) => () =>
|
||||
{
|
||||
const newChipList = [...chips];
|
||||
newChipList.splice(newChipList.indexOf(item), 1);
|
||||
setChips(newChipList);
|
||||
};
|
||||
|
||||
function handleInputChange(event: { target: { value: React.SetStateAction<string>; }; })
|
||||
{
|
||||
setInputValue(event.target.value);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Downshift
|
||||
id="downshift-multiple"
|
||||
inputValue={inputValue}
|
||||
onChange={handleChange}
|
||||
selectedItem={chips}
|
||||
>
|
||||
{({getInputProps}) =>
|
||||
{
|
||||
const {onBlur, onChange, onFocus} = getInputProps({
|
||||
onKeyDown: handleKeyDown,
|
||||
placeholder
|
||||
});
|
||||
// @ts-ignore
|
||||
return (
|
||||
<div id="chip-text-field-container" style={{flexWrap: "wrap", display:"flex"}}>
|
||||
<TextField
|
||||
sx={{width: "100%"}}
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
InputProps={{
|
||||
startAdornment:
|
||||
<div>
|
||||
{
|
||||
chips.map((item, i) => (
|
||||
<Chip
|
||||
color={(chipType !== "number" || ! Number.isNaN(Number(item))) ? "info" : "error"}
|
||||
key={`${item}-${i}`}
|
||||
variant="outlined"
|
||||
tabIndex={-1}
|
||||
label={item}
|
||||
className={classes.chip}
|
||||
/>
|
||||
|
||||
))
|
||||
}
|
||||
</div>,
|
||||
onBlur,
|
||||
multiline,
|
||||
rows,
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
handleInputChange(event);
|
||||
onChange(event);
|
||||
},
|
||||
onFocus,
|
||||
placeholder,
|
||||
onKeyDown: handleKeyDown
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Downshift>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
ChipTextField.defaultProps = {
|
||||
chipData: []
|
||||
};
|
||||
ChipTextField.propTypes = {
|
||||
handleChipChange: func.isRequired,
|
||||
chipData: arrayOf(string)
|
||||
};
|
||||
|
||||
export default ChipTextField
|
@ -21,16 +21,434 @@
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {TextFieldProps} from "@mui/material";
|
||||
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 {getGridNumericOperators, getGridStringOperators, GridColDef, GridFilterInputValueProps, GridFilterItem} from "@mui/x-data-grid-pro";
|
||||
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/QButtons";
|
||||
import QDynamicSelect from "qqq/components/QDynamicSelect/QDynamicSelect";
|
||||
import ChipTextField from "qqq/pages/entity-list/ChipTextField";
|
||||
|
||||
|
||||
////////////////////////////////
|
||||
// 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 //
|
||||
@ -67,12 +485,25 @@ const stringNotEndWithOperator: GridFilterOperator = {
|
||||
InputComponent: GridFilterInputValue,
|
||||
};
|
||||
|
||||
const stringIsAnyOfOperator: GridFilterOperator = {
|
||||
label: "is any of",
|
||||
value: "isAnyOf",
|
||||
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];
|
||||
gridStringOperators = [ equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators ];
|
||||
|
||||
///////////////////////////////////
|
||||
// remove default isany operator //
|
||||
///////////////////////////////////
|
||||
gridStringOperators.splice(2, 1)[0];
|
||||
gridStringOperators = [ equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators, stringIsAnyOfOperator ];
|
||||
|
||||
export const QGridStringOperators = gridStringOperators;
|
||||
|
||||
@ -184,8 +615,20 @@ const notBetweenOperator: GridFilterOperator = {
|
||||
InputComponent: InputNumberInterval
|
||||
};
|
||||
|
||||
export const QGridNumericOperators = [ ...getGridNumericOperators(), betweenOperator, notBetweenOperator ];
|
||||
const numericIsAnyOfOperator: GridFilterOperator = {
|
||||
label: "is any of",
|
||||
value: "isAnyOf",
|
||||
getApplyFilterFn: () => null,
|
||||
// @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 ];
|
||||
|
||||
///////////////////////
|
||||
// boolean operators //
|
||||
@ -312,7 +755,7 @@ export const buildQGridPvsOperators = (tableName: string, field: QFieldMetaData)
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
},
|
||||
{
|
||||
label: "is not empty",
|
||||
label: "is not Empty",
|
||||
value: "isNotEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
}
|
||||
|
@ -37,31 +37,9 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import {
|
||||
DataGridPro,
|
||||
getGridDateOperators,
|
||||
GridCallbackDetails,
|
||||
GridColDef,
|
||||
GridColumnOrderChangeParams,
|
||||
GridColumnVisibilityModel,
|
||||
GridExportMenuItemProps,
|
||||
GridFilterModel,
|
||||
GridLinkOperator,
|
||||
GridRowId,
|
||||
GridRowParams,
|
||||
GridRowsProp,
|
||||
GridSelectionModel,
|
||||
GridSortItem,
|
||||
GridSortModel,
|
||||
GridToolbarColumnsButton,
|
||||
GridToolbarContainer,
|
||||
GridToolbarDensitySelector,
|
||||
GridToolbarExportContainer,
|
||||
GridToolbarFilterButton,
|
||||
MuiEvent
|
||||
} from "@mui/x-data-grid-pro";
|
||||
import {DataGridPro, getGridDateOperators, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterModel, GridLinkOperator, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
import {Link, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import DashboardLayout from "qqq/components/DashboardLayout";
|
||||
@ -81,6 +59,7 @@ const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility";
|
||||
const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
|
||||
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
||||
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -187,6 +166,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
let defaultSort = [] as GridSortItem[];
|
||||
let defaultVisibility = {};
|
||||
let defaultRowsPerPage = 10;
|
||||
let defaultDensity = "standard";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// set the to be not per table (do as above if we want per table) at a later port //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
const densityLocalStorageKey = `${DENSITY_LOCAL_STORAGE_KEY_ROOT}`;
|
||||
|
||||
if (localStorage.getItem(sortLocalStorageKey))
|
||||
{
|
||||
@ -200,11 +185,16 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
||||
}
|
||||
if (localStorage.getItem(densityLocalStorageKey))
|
||||
{
|
||||
defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey));
|
||||
}
|
||||
|
||||
const [ filterModel, setFilterModel ] = useState({items: []} as GridFilterModel);
|
||||
const [ columnSortModel, setColumnSortModel ] = useState(defaultSort);
|
||||
const [ columnVisibilityModel, setColumnVisibilityModel ] = useState(defaultVisibility);
|
||||
const [ rowsPerPage, setRowsPerPage ] = useState(defaultRowsPerPage);
|
||||
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage);
|
||||
const [density, setDensity] = useState(defaultDensity as GridDensity);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for some reason, if we set the filterModel to what is in local storage, an onChange event //
|
||||
@ -214,28 +204,29 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const [ defaultFilter ] = useState({items: []} as GridFilterModel);
|
||||
|
||||
const [ tableState, setTableState ] = useState("");
|
||||
const [ tableMetaData, setTableMetaData ] = useState(null as QTableMetaData);
|
||||
const [ defaultFilterLoaded, setDefaultFilterLoaded ] = useState(false);
|
||||
const [ , setFiltersMenu ] = useState(null);
|
||||
const [ actionsMenu, setActionsMenu ] = useState(null);
|
||||
const [ tableProcesses, setTableProcesses ] = useState([] as QProcessMetaData[]);
|
||||
const [ allTableProcesses, setAllTableProcesses ] = useState([] as QProcessMetaData[]);
|
||||
const [ pageNumber, setPageNumber ] = useState(0);
|
||||
const [ totalRecords, setTotalRecords ] = useState(0);
|
||||
const [ selectedIds, setSelectedIds ] = useState([] as string[]);
|
||||
const [ selectFullFilterState, setSelectFullFilterState ] = useState("n/a" as "n/a" | "checked" | "filter");
|
||||
const [ columns, setColumns ] = useState([] as GridColDef[]);
|
||||
const [ rows, setRows ] = useState([] as GridRowsProp[]);
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const [ alertContent, setAlertContent ] = useState("");
|
||||
const [ tableLabel, setTableLabel ] = useState("");
|
||||
const [ gridMouseDownX, setGridMouseDownX ] = useState(0);
|
||||
const [ gridMouseDownY, setGridMouseDownY ] = useState(0);
|
||||
const [ pinnedColumns, setPinnedColumns ] = useState({left: [ "__check__", "id" ]});
|
||||
const [tableState, setTableState] = useState("");
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false);
|
||||
const [, setFiltersMenu] = useState(null);
|
||||
const [actionsMenu, setActionsMenu] = useState(null);
|
||||
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
||||
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
const [selectedIds, setSelectedIds] = useState([] as string[]);
|
||||
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter");
|
||||
const [columns, setColumns] = useState([] as GridColDef[]);
|
||||
const [rows, setRows] = useState([] as GridRowsProp[]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [alertContent, setAlertContent] = useState("");
|
||||
const [tableLabel, setTableLabel] = useState("");
|
||||
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
||||
const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]});
|
||||
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
||||
|
||||
const [ activeModalProcess, setActiveModalProcess ] = useState(null as QProcessMetaData);
|
||||
const [ launchingProcess, setLaunchingProcess ] = useState(launchProcess);
|
||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||
|
||||
const instance = useRef({timer: null});
|
||||
|
||||
@ -313,6 +304,14 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
filterModel.items.forEach((item) =>
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// if no value set and not 'empty' or 'not empty' operators, skip this filter //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(! item.value && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
||||
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
||||
@ -434,7 +433,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
delete countResults[latestQueryId];
|
||||
}, [ receivedCountTimestamp ]);
|
||||
|
||||
|
||||
///////////////////////////
|
||||
// display query results //
|
||||
///////////////////////////
|
||||
@ -623,8 +621,28 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
|
||||
};
|
||||
|
||||
const handleStateChange = (state: GridState, event: MuiEvent, details: GridCallbackDetails) =>
|
||||
{
|
||||
if (state && state.density && state.density.value !== density)
|
||||
{
|
||||
setDensity(state.density.value);
|
||||
localStorage.setItem(densityLocalStorageKey, JSON.stringify(state.density.value));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// if a grid preference window is open, ignore and reset timer //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
console.log(gridPreferencesWindow);
|
||||
if (gridPreferencesWindow !== undefined)
|
||||
{
|
||||
clearTimeout(instance.current.timer);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// strategy for when to trigger or not trigger a row click: //
|
||||
// To avoid a drag-event that highlighted text in a cell: //
|
||||
@ -635,35 +653,22 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
// - also avoid a click, then click-again-and-start-dragging, by always cancelling the timer in mouse-down. //
|
||||
// All in, these seem to have good results - the only downside being the half-second delay... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
navigate(`${params.id}`);
|
||||
/*
|
||||
const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY));
|
||||
if (diff < 5)
|
||||
{
|
||||
console.log("clearing timeout");
|
||||
clearTimeout(instance.current.timer);
|
||||
instance.current.timer = setTimeout(() =>
|
||||
{
|
||||
navigate(`${params.id}`);
|
||||
}, 500);
|
||||
}, 100);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
const handleGridMouseDown = useCallback((event: any) =>
|
||||
{
|
||||
setGridMouseDownX(event.clientX);
|
||||
setGridMouseDownY(event.clientY);
|
||||
clearTimeout(instance.current.timer);
|
||||
}, []);
|
||||
|
||||
const handleGridDoubleClick = useCallback((event: any) =>
|
||||
{
|
||||
clearTimeout(instance.current.timer);
|
||||
}, []);
|
||||
|
||||
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) =>
|
||||
{
|
||||
@ -705,6 +710,26 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
const handleFilterChange = (filterModel: GridFilterModel) =>
|
||||
{
|
||||
////////////////////////////////////////////////////////
|
||||
// remove any items in the filter that are incomplete //
|
||||
////////////////////////////////////////////////////////
|
||||
/*
|
||||
if(filterModel && filterModel.items)
|
||||
{
|
||||
filterModel.items.forEach((item: GridFilterItem, index: number, arr: GridFilterItem[]) =>
|
||||
{
|
||||
if(! item.value)
|
||||
{
|
||||
if( item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
|
||||
{
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
setFilterModel(filterModel);
|
||||
if (filterLocalStorageKey)
|
||||
{
|
||||
@ -977,6 +1002,36 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
function CustomToolbar()
|
||||
{
|
||||
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
|
||||
params, // GridRowParams
|
||||
event, // MuiEvent<React.MouseEvent<HTMLElement>>
|
||||
details, // GridCallbackDetails
|
||||
) =>
|
||||
{
|
||||
setGridMouseDownX(event.clientX);
|
||||
setGridMouseDownY(event.clientY);
|
||||
clearTimeout(instance.current.timer);
|
||||
};
|
||||
|
||||
const handleDoubleClick: GridEventListener<"rowDoubleClick"> = (event: any) =>
|
||||
{
|
||||
clearTimeout(instance.current.timer);
|
||||
};
|
||||
|
||||
|
||||
const apiRef = useGridApiContext();
|
||||
useGridApiEventHandler(apiRef, "cellMouseDown", handleMouseDown);
|
||||
useGridApiEventHandler(apiRef, "rowDoubleClick", handleDoubleClick);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// keep track of any preference windows that are opened in the toolbar, to allow ignoring clicks away from the window //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
const preferencePanelState = useGridSelector(apiRef, gridPreferencePanelStateSelector);
|
||||
setGridPreferencesWindow(preferencePanelState.openedPanelValue);
|
||||
});
|
||||
|
||||
return (
|
||||
<GridToolbarContainer>
|
||||
<div>
|
||||
@ -1093,6 +1148,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
}, [ pageNumber, rowsPerPage ]);
|
||||
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Navbar />
|
||||
@ -1130,8 +1186,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
</MDBox>
|
||||
<Card>
|
||||
{/* with these turned on, the toolbar & pagination controls become very flaky...
|
||||
onMouseDown={(e) => handleGridMouseDown(e)} onDoubleClick={(e) => handleGridDoubleClick(e)} */}
|
||||
<MDBox height="100%">
|
||||
<DataGridPro
|
||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
||||
@ -1150,7 +1204,8 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
rowCount={totalRecords === null ? 0 : totalRecords}
|
||||
onPageSizeChange={handleRowsPerPageChange}
|
||||
onRowClick={handleRowClick}
|
||||
density="standard"
|
||||
onStateChange={handleStateChange}
|
||||
density={density}
|
||||
loading={loading}
|
||||
filterModel={filterModel}
|
||||
onFilterModelChange={handleFilterChange}
|
||||
|
@ -183,3 +183,32 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
{
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#chip-text-field-container > div > div
|
||||
{
|
||||
background: #F8F8F8;
|
||||
height: 330px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#chip-text-field-container > div > div > textarea
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chip-text-field-container > div > div > div
|
||||
{
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#outlined-multiline-static
|
||||
{
|
||||
padding-left: 10px;
|
||||
padding-top: 0px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
Reference in New Issue
Block a user