diff --git a/src/qqq/components/query/FilterCriteriaPaster.tsx b/src/qqq/components/query/FilterCriteriaPaster.tsx
new file mode 100644
index 0000000..b594d02
--- /dev/null
+++ b/src/qqq/components/query/FilterCriteriaPaster.tsx
@@ -0,0 +1,440 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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 .
+ */
+
+import {FormControl, InputLabel, Select, SelectChangeEvent} 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 {GridFilterItem} from "@mui/x-data-grid-pro";
+import React, {useEffect, useState} from "react";
+import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
+import ChipTextField from "qqq/components/forms/ChipTextField";
+
+interface Props
+{
+ type: string;
+}
+
+FilterCriteriaPaster.defaultProps = {};
+
+function FilterCriteriaPaster({type}: Props): JSX.Element
+{
+ 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";
+
+ //x 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)}`);
+ // todo!
+ // setGridFilterItem(item);
+ // props.applyValue(item);
+ };
+
+ const clearData = () =>
+ {
+ setDelimiter("");
+ setDelimiterCharacter("");
+ setChipData([]);
+ setInputText("");
+ setDetectedText("");
+ setCustomDelimiterValue("");
+ setPasteModalIsOpen(false);
+ };
+
+ const handleCancelClicked = () =>
+ {
+ clearData();
+ setPasteModalIsOpen(false);
+ };
+
+ const handleSaveClicked = () =>
+ {
+ //x if (gridFilterItem)
+ /* todo
+ {
+ ////////////////////////////////////////
+ // 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 (
+
+
+ paste_content
+
+ {
+ pasteModalIsOpen &&
+ (
+
+
+
+
+
+
+
+ Bulk Add Filter Values
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ }}
+ chipData={chipData}
+ chipType={type}
+ multiline
+ fullWidth
+ variant="outlined"
+ id="tags"
+ rows={0}
+ name="tags"
+ label="FILTER VALUES REVIEW"
+ />
+
+
+
+
+
+
+
+
+ SEPARATOR
+
+
+
+ {delimiter === Delimiter.CUSTOM.valueOf() && (
+
+
+
+
+ )}
+ {inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
+
+
+ {detectedText}
+
+ )}
+
+
+
+ {
+ errorText && chipData.length > 0 && (
+
+ error
+ {errorText}
+
+ )
+ }
+
+
+ {
+ chipData && chipData.length > 0 && (
+ {chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ );
+}
+
+export default FilterCriteriaPaster;