From 5c274a0a8a279f63727065233a0a7e463c779032 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 5 Jun 2023 09:45:27 -0500 Subject: [PATCH] Initial checkin --- .../components/query/FilterCriteriaPaster.tsx | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 src/qqq/components/query/FilterCriteriaPaster.tsx 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;