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:
1597
package-lock.json
generated
1597
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",
|
"@testing-library/user-event": "13.5.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/node": "16.11.21",
|
"@types/node": "16.11.21",
|
||||||
|
"@types/prop-types": "^15.7.5",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/react-router-hash-link": "2.4.5",
|
"@types/react-router-hash-link": "2.4.5",
|
||||||
"chart.js": "3.4.1",
|
"chart.js": "3.4.1",
|
||||||
"chroma-js": "2.4.2",
|
"chroma-js": "2.4.2",
|
||||||
"datejs": "1.0.0-rc3",
|
"datejs": "1.0.0-rc3",
|
||||||
|
"downshift": "3.2.10",
|
||||||
"dropzone": "5.9.2",
|
"dropzone": "5.9.2",
|
||||||
"flatpickr": "4.6.9",
|
"flatpickr": "4.6.9",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
|
@ -44,15 +44,20 @@ export function QCreateNewButton(): JSX.Element
|
|||||||
|
|
||||||
interface QSaveButtonProps
|
interface QSaveButtonProps
|
||||||
{
|
{
|
||||||
|
label?: string;
|
||||||
|
onClickHandler?: any,
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
QSaveButton.defaultProps = {
|
||||||
|
label: "Save"
|
||||||
|
};
|
||||||
|
|
||||||
export function QSaveButton({disabled}: QSaveButtonProps): JSX.Element
|
export function QSaveButton({label, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<MDBox ml={3} width={standardWidth}>
|
<MDBox ml={3} width={standardWidth}>
|
||||||
<MDButton type="submit" variant="gradient" color="info" size="small" fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}>
|
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}>
|
||||||
Save
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
</MDBox>
|
</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 {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
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 Box from "@mui/material/Box";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Modal from "@mui/material/Modal";
|
||||||
import TextField from "@mui/material/TextField";
|
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 {GridFilterInputValue} from "@mui/x-data-grid/components/panel/filterPanel/GridFilterInputValue";
|
||||||
import {GridApiCommunity} from "@mui/x-data-grid/internals";
|
import {GridApiCommunity} from "@mui/x-data-grid/internals";
|
||||||
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
|
import {QCancelButton, QSaveButton} from "qqq/components/QButtons";
|
||||||
import QDynamicSelect from "qqq/components/QDynamicSelect/QDynamicSelect";
|
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 //
|
// string operators //
|
||||||
@ -67,12 +485,25 @@ const stringNotEndWithOperator: GridFilterOperator = {
|
|||||||
InputComponent: GridFilterInputValue,
|
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 gridStringOperators = getGridStringOperators();
|
||||||
let equals = gridStringOperators.splice(1, 1)[0];
|
let equals = gridStringOperators.splice(1, 1)[0];
|
||||||
let contains = gridStringOperators.splice(0, 1)[0];
|
let contains = gridStringOperators.splice(0, 1)[0];
|
||||||
let startsWith = gridStringOperators.splice(0, 1)[0];
|
let startsWith = gridStringOperators.splice(0, 1)[0];
|
||||||
let endsWith = 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;
|
export const QGridStringOperators = gridStringOperators;
|
||||||
|
|
||||||
@ -184,8 +615,20 @@ const notBetweenOperator: GridFilterOperator = {
|
|||||||
InputComponent: InputNumberInterval
|
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 //
|
// boolean operators //
|
||||||
@ -312,9 +755,9 @@ export const buildQGridPvsOperators = (tableName: string, field: QFieldMetaData)
|
|||||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "is not empty",
|
label: "is not Empty",
|
||||||
value: "isNotEmpty",
|
value: "isNotEmpty",
|
||||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
@ -37,31 +37,9 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
import {
|
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";
|
||||||
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 {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
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 {Link, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import DashboardLayout from "qqq/components/DashboardLayout";
|
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 COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
|
||||||
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
||||||
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
||||||
|
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -187,6 +166,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
let defaultVisibility = {};
|
let defaultVisibility = {};
|
||||||
let defaultRowsPerPage = 10;
|
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))
|
if (localStorage.getItem(sortLocalStorageKey))
|
||||||
{
|
{
|
||||||
@ -200,11 +185,16 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
||||||
}
|
}
|
||||||
|
if (localStorage.getItem(densityLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
const [ filterModel, setFilterModel ] = useState({items: []} as GridFilterModel);
|
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||||
const [ columnSortModel, setColumnSortModel ] = useState(defaultSort);
|
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||||
const [ columnVisibilityModel, setColumnVisibilityModel ] = useState(defaultVisibility);
|
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
||||||
const [ rowsPerPage, setRowsPerPage ] = useState(defaultRowsPerPage);
|
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 //
|
// 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 [ defaultFilter ] = useState({items: []} as GridFilterModel);
|
||||||
|
|
||||||
const [ tableState, setTableState ] = useState("");
|
const [tableState, setTableState] = useState("");
|
||||||
const [ tableMetaData, setTableMetaData ] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
const [ defaultFilterLoaded, setDefaultFilterLoaded ] = useState(false);
|
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false);
|
||||||
const [ , setFiltersMenu ] = useState(null);
|
const [, setFiltersMenu] = useState(null);
|
||||||
const [ actionsMenu, setActionsMenu ] = useState(null);
|
const [actionsMenu, setActionsMenu] = useState(null);
|
||||||
const [ tableProcesses, setTableProcesses ] = useState([] as QProcessMetaData[]);
|
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
const [ allTableProcesses, setAllTableProcesses ] = useState([] as QProcessMetaData[]);
|
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
const [ pageNumber, setPageNumber ] = useState(0);
|
const [pageNumber, setPageNumber] = useState(0);
|
||||||
const [ totalRecords, setTotalRecords ] = useState(0);
|
const [totalRecords, setTotalRecords] = useState(0);
|
||||||
const [ selectedIds, setSelectedIds ] = useState([] as string[]);
|
const [selectedIds, setSelectedIds] = useState([] as string[]);
|
||||||
const [ selectFullFilterState, setSelectFullFilterState ] = useState("n/a" as "n/a" | "checked" | "filter");
|
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter");
|
||||||
const [ columns, setColumns ] = useState([] as GridColDef[]);
|
const [columns, setColumns] = useState([] as GridColDef[]);
|
||||||
const [ rows, setRows ] = useState([] as GridRowsProp[]);
|
const [rows, setRows] = useState([] as GridRowsProp[]);
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [ alertContent, setAlertContent ] = useState("");
|
const [alertContent, setAlertContent] = useState("");
|
||||||
const [ tableLabel, setTableLabel ] = useState("");
|
const [tableLabel, setTableLabel] = useState("");
|
||||||
const [ gridMouseDownX, setGridMouseDownX ] = useState(0);
|
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||||
const [ gridMouseDownY, setGridMouseDownY ] = useState(0);
|
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
||||||
const [ pinnedColumns, setPinnedColumns ] = useState({left: [ "__check__", "id" ]});
|
const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]});
|
||||||
|
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
||||||
|
|
||||||
const [ activeModalProcess, setActiveModalProcess ] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
const [ launchingProcess, setLaunchingProcess ] = useState(launchProcess);
|
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||||
|
|
||||||
const instance = useRef({timer: null});
|
const instance = useRef({timer: null});
|
||||||
|
|
||||||
@ -313,6 +304,14 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
filterModel.items.forEach((item) =>
|
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 operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
||||||
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
||||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
||||||
@ -434,7 +433,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
delete countResults[latestQueryId];
|
delete countResults[latestQueryId];
|
||||||
}, [ receivedCountTimestamp ]);
|
}, [ receivedCountTimestamp ]);
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
// display query results //
|
// display query results //
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
@ -623,8 +621,28 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
|
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) =>
|
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: //
|
// strategy for when to trigger or not trigger a row click: //
|
||||||
// To avoid a drag-event that highlighted text in a cell: //
|
// 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. //
|
// - 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... //
|
// 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));
|
const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY));
|
||||||
if (diff < 5)
|
if (diff < 5)
|
||||||
{
|
{
|
||||||
|
console.log("clearing timeout");
|
||||||
clearTimeout(instance.current.timer);
|
clearTimeout(instance.current.timer);
|
||||||
instance.current.timer = setTimeout(() =>
|
instance.current.timer = setTimeout(() =>
|
||||||
{
|
{
|
||||||
navigate(`${params.id}`);
|
navigate(`${params.id}`);
|
||||||
}, 500);
|
}, 100);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
|
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) =>
|
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) =>
|
||||||
{
|
{
|
||||||
@ -705,6 +710,26 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleFilterChange = (filterModel: GridFilterModel) =>
|
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);
|
setFilterModel(filterModel);
|
||||||
if (filterLocalStorageKey)
|
if (filterLocalStorageKey)
|
||||||
{
|
{
|
||||||
@ -803,7 +828,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
</style>
|
</style>
|
||||||
<title>${filename}</title>
|
<title>${filename}</title>
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
window.location.href="${url}";
|
window.location.href="${url}";
|
||||||
}, 1);
|
}, 1);
|
||||||
@ -977,6 +1002,36 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
function CustomToolbar()
|
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 (
|
return (
|
||||||
<GridToolbarContainer>
|
<GridToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
@ -1093,6 +1148,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
document.scrollingElement.scrollTop = 0;
|
document.scrollingElement.scrollTop = 0;
|
||||||
}, [ pageNumber, rowsPerPage ]);
|
}, [ pageNumber, rowsPerPage ]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -1130,8 +1186,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
</MDBox>
|
</MDBox>
|
||||||
<Card>
|
<Card>
|
||||||
{/* with these turned on, the toolbar & pagination controls become very flaky...
|
|
||||||
onMouseDown={(e) => handleGridMouseDown(e)} onDoubleClick={(e) => handleGridDoubleClick(e)} */}
|
|
||||||
<MDBox height="100%">
|
<MDBox height="100%">
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
||||||
@ -1150,7 +1204,8 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
rowCount={totalRecords === null ? 0 : totalRecords}
|
rowCount={totalRecords === null ? 0 : totalRecords}
|
||||||
onPageSizeChange={handleRowsPerPageChange}
|
onPageSizeChange={handleRowsPerPageChange}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
density="standard"
|
onStateChange={handleStateChange}
|
||||||
|
density={density}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
filterModel={filterModel}
|
filterModel={filterModel}
|
||||||
onFilterModelChange={handleFilterChange}
|
onFilterModelChange={handleFilterChange}
|
||||||
|
@ -182,4 +182,33 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
.modalProcess>.MuiBox-root>.MuiBox-root
|
.modalProcess>.MuiBox-root>.MuiBox-root
|
||||||
{
|
{
|
||||||
margin-bottom: 24px;
|
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