Merge branch 'integration/sprint-28' into feature/CTLE-214-dot-menu

This commit is contained in:
Tim Chamberlain
2023-06-22 10:17:11 -05:00
26 changed files with 2469 additions and 272 deletions

View File

@ -118,12 +118,12 @@ function ChipTextField({...props})
return (
<div id="chip-text-field-container" style={{flexWrap: "wrap", display:"flex"}}>
<TextField
sx={{width: "100%"}}
sx={{width: "99%"}}
disabled={disabled}
label={label}
InputProps={{
startAdornment:
<div>
<div style={{overflowY: "auto", overflowX: "hidden", margin: "-10px", width: "calc(100% + 20px)", padding: "10px", marginBottom: "-20px", height: "calc(100% + 10px)"}}>
{
chips.map((item, i) => (
<Chip

View File

@ -38,6 +38,7 @@ interface Props
tableName?: string;
processName?: string;
fieldName: string;
overrideId?: string;
fieldLabel: string;
inForm: boolean;
initialValue?: any;
@ -70,29 +71,34 @@ DynamicSelect.defaultProps = {
const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
{
const [ open, setOpen ] = useState(false);
const [ options, setOptions ] = useState<readonly QPossibleValue[]>([]);
const [ searchTerm, setSearchTerm ] = useState(null);
const [ firstRender, setFirstRender ] = useState(true);
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true);
////////////////////////////////////////////////////////////////////////////////////////////////
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
// else non-multiple, assume we took in an initialValue (id) and initialDisplayValue (label), //
// and build a little object that looks like a possibleValue out of those //
////////////////////////////////////////////////////////////////////////////////////////////////
const [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined)
let [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined)
: useState(initialValue && initialDisplayValue ? [{id: initialValue, label: initialDisplayValue}] : null);
if (isMultiple && defaultValue === null)
{
defaultValue = [];
}
// const loading = open && options.length === 0;
const [loading, setLoading] = useState(false);
const [ switchChecked, setSwitchChecked ] = useState(false);
const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode);
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null;
if(inForm)
if (inForm)
{
const {setFieldValue} = useFormikContext();
setFieldValueRef = setFieldValue;
@ -239,9 +245,11 @@ function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, i
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
};
// console.log(`default value: ${JSON.stringify(defaultValue)}`);
const autocomplete = (
<Autocomplete
id={fieldName}
id={overrideId ?? fieldName}
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
open={open}
fullWidth
@ -291,6 +299,8 @@ function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, i
disabled={isDisabled}
multiple={isMultiple}
disableCloseOnSelect={isMultiple}
limitTags={5}
slotProps={{popper: {className: "DynamicSelectPopper"}}}
renderInput={(params) => (
<TextField
{...params}

View File

@ -54,6 +54,7 @@ interface Props
closeModalHandler?: (event: object, reason: string) => void;
defaultValues: { [key: string]: string };
disabledFields: { [key: string]: boolean } | string[];
isDuplicate?: boolean;
}
EntityForm.defaultProps = {
@ -63,6 +64,7 @@ EntityForm.defaultProps = {
closeModalHandler: null,
defaultValues: {},
disabledFields: {},
isDuplicate: false
};
function EntityForm(props: Props): JSX.Element
@ -133,6 +135,15 @@ function EntityForm(props: Props): JSX.Element
for (let i = 0; i < formFields.length; i++)
{
formData.formFields[formFields[i].name] = formFields[i];
if (formFields[i].possibleValueProps)
{
formFields[i].possibleValueProps.otherValues = formFields[i].possibleValueProps.otherValues ?? new Map<string, any>();
Object.keys(formFields).forEach((otherKey) =>
{
formFields[i].possibleValueProps.otherValues.set(otherKey, values[otherKey]);
});
}
}
if (!Object.keys(formFields).length)
@ -164,24 +175,30 @@ function EntityForm(props: Props): JSX.Element
fieldArray.push(fieldMetaData);
});
/////////////////////////////////////////////////////////////////////////////////
// if doing an edit, fetch the record and pre-populate the form values from it //
/////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
// if doing an edit or duplicate, fetch the record and pre-populate the form values from it //
//////////////////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (props.id !== null)
{
record = await qController.get(tableName, props.id);
setRecord(record);
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
const titleVerb = props.isDuplicate ? "Duplicate" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!props.isModal)
{
setPageHeader(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
}
tableMetaData.fields.forEach((fieldMetaData, key) =>
{
if (props.isDuplicate && fieldMetaData.name == tableMetaData.primaryKeyField)
{
return;
}
initialValues[key] = record.values.get(key);
});
@ -206,15 +223,6 @@ function EntityForm(props: Props): JSX.Element
setPageHeader(`Creating New ${tableMetaData?.label}`);
}
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. //
////////////////////////////////////////////////////////////////////////////////////////////////
@ -245,6 +253,32 @@ function EntityForm(props: Props): JSX.Element
}
}
//////////////////////////////////////
// check capabilities & permissions //
//////////////////////////////////////
if (props.isDuplicate || !props.id)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
}
}
else
{
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
/////////////////////////////////////////////////////////////////////
// make sure all initialValues are properly formatted for the form //
/////////////////////////////////////////////////////////////////////
@ -307,11 +341,11 @@ function EntityForm(props: Props): JSX.Element
const fieldName = section.fieldNames[j];
const field = tableMetaData.fields.get(fieldName);
////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////
if (props.id !== null || field.isEditable)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not duplicating) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isDuplicate) || field.isEditable)
{
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
}
@ -359,7 +393,12 @@ function EntityForm(props: Props): JSX.Element
// but if the user used the anchors on the page, this doesn't effectively cancel... //
// what we have here pushed a new history entry (I think?), so could be better //
///////////////////////////////////////////////////////////////////////////////////////
if (props.id !== null)
if (props.id !== null && props.isDuplicate)
{
const path = `${location.pathname.replace(/\/duplicate$/, "")}`;
navigate(path, {replace: true});
}
else if (props.id !== null)
{
const path = `${location.pathname.replace(/\/edit$/, "")}`;
navigate(path, {replace: true});
@ -419,8 +458,9 @@ function EntityForm(props: Props): JSX.Element
}
}
if (props.id !== null)
if (props.id !== null && !props.isDuplicate)
{
// todo - audit that it's a dupe
await qController
.update(tableName, props.id, values)
.then((record) =>
@ -464,7 +504,9 @@ function EntityForm(props: Props): JSX.Element
}
else
{
const path = location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
const path = props.isDuplicate ?
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true}});
}
})
@ -472,8 +514,9 @@ function EntityForm(props: Props): JSX.Element
{
if(error.message.toLowerCase().startsWith("warning"))
{
const path = location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path);
const path = props.isDuplicate ?
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true, warning: error.message}});
}
else

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {Box, FormControlLabel, FormGroup} from "@mui/material";
import Button from "@mui/material/Button";
@ -37,6 +38,7 @@ declare module "@mui/x-data-grid"
interface ColumnsPanelPropsOverrides
{
tableMetaData: QTableMetaData;
metaData: QInstance;
initialOpenedGroups: { [name: string]: boolean };
openGroupsChanger: (openedGroups: { [name: string]: boolean }) => void;
initialFilterText: string;
@ -70,7 +72,11 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
{
for (let i = 0; i < props.tableMetaData.exposedJoins.length; i++)
{
tables.push(props.tableMetaData.exposedJoins[i].joinTable);
const exposedJoin = props.tableMetaData.exposedJoins[i];
if (props.metaData.tables.has(exposedJoin.joinTable.name))
{
tables.push(exposedJoin.joinTable);
}
}
}
@ -112,7 +118,7 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
return (true);
}
}
catch(e)
catch (e)
{
//////////////////////////////////////////////////////////////////////////////////
// in case text is an invalid regex... well, at least do a starts-with match... //
@ -123,6 +129,33 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
}
}
const tableLabel = column.headerName.replace(/:.*/, "");
if (tableLabel)
{
try
{
////////////////////////////////////////////////////////////
// try to match word-boundary followed by the filter text //
// e.g., "name" would match "First Name" or "Last Name" //
////////////////////////////////////////////////////////////
const re = new RegExp("\\b" + filterText.toLowerCase());
if (tableLabel.toLowerCase().match(re))
{
return (true);
}
}
catch (e)
{
//////////////////////////////////////////////////////////////////////////////////
// in case text is an invalid regex... well, at least do a starts-with match... //
//////////////////////////////////////////////////////////////////////////////////
if (tableLabel.toLowerCase().startsWith(filterText.toLowerCase()))
{
return (true);
}
}
}
return (false);
};

View File

@ -0,0 +1,192 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button/Button";
import Icon from "@mui/material/Icon/Icon";
import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro";
import React, {forwardRef, useReducer} from "react";
import {FilterCriteriaRow, getDefaultCriteriaValue} from "qqq/components/query/FilterCriteriaRow";
declare module "@mui/x-data-grid"
{
///////////////////////////////////////////////////////////////////////
// this lets these props be passed in via <DataGrid componentsProps> //
///////////////////////////////////////////////////////////////////////
interface FilterPanelPropsOverrides
{
tableMetaData: QTableMetaData;
metaData: QInstance;
queryFilter: QQueryFilter;
updateFilter: (newFilter: QQueryFilter) => void;
}
}
export class QFilterCriteriaWithId extends QFilterCriteria
{
id: number
}
let debounceTimeout: string | number | NodeJS.Timeout;
let criteriaId = (new Date().getTime()) + 1000;
export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
function MyCustomFilterPanel(props: GridSlotsComponentsProps["filterPanel"], ref)
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const queryFilter = props.queryFilter;
// console.log(`CustomFilterPanel: filter: ${JSON.stringify(queryFilter)}`);
function focusLastField()
{
setTimeout(() =>
{
try
{
// console.log(`Try to focus ${criteriaId - 1}`);
document.getElementById(`field-${criteriaId - 1}`).focus();
}
catch (e)
{
console.log("Error trying to focus field ...", e);
}
});
}
const addCriteria = () =>
{
const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, getDefaultCriteriaValue());
qFilterCriteriaWithId.id = criteriaId++;
console.log(`adding criteria id ${qFilterCriteriaWithId.id}`);
queryFilter.criteria.push(qFilterCriteriaWithId);
props.updateFilter(queryFilter);
forceUpdate();
focusLastField();
};
if (!queryFilter.criteria)
{
queryFilter.criteria = [];
addCriteria();
}
if (queryFilter.criteria.length == 0)
{
/////////////////////////////////////////////
// make sure there's at least one criteria //
/////////////////////////////////////////////
addCriteria();
}
else
{
////////////////////////////////////////////////////////////////////////////////////
// make sure all criteria have an id on them (to be used as react component keys) //
////////////////////////////////////////////////////////////////////////////////////
let updatedAny = false;
for (let i = 0; i < queryFilter.criteria.length; i++)
{
if (!queryFilter.criteria[i].id)
{
queryFilter.criteria[i].id = criteriaId++;
}
}
if (updatedAny)
{
props.updateFilter(queryFilter);
}
}
if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName)
{
focusLastField();
}
let booleanOperator: "AND" | "OR" | null = null;
if (queryFilter.criteria.length > 1)
{
booleanOperator = queryFilter.booleanOperator;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// needDebounce param - things like typing in a text field DO need debounce, but changing an operator doesn't //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const updateCriteria = (newCriteria: QFilterCriteria, index: number, needDebounce = false) =>
{
queryFilter.criteria[index] = newCriteria;
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(() => props.updateFilter(queryFilter), needDebounce ? 500 : 1);
forceUpdate();
};
const updateBooleanOperator = (newValue: string) =>
{
queryFilter.booleanOperator = newValue;
props.updateFilter(queryFilter);
forceUpdate();
};
const removeCriteria = (index: number) =>
{
queryFilter.criteria.splice(index, 1);
props.updateFilter(queryFilter);
forceUpdate();
};
return (
<Box className="customFilterPanel">
{
queryFilter.criteria.map((criteria: QFilterCriteriaWithId, index: number) =>
(
<Box key={criteria.id}>
<FilterCriteriaRow
id={criteria.id}
index={index}
tableMetaData={props.tableMetaData}
metaData={props.metaData}
criteria={criteria}
booleanOperator={booleanOperator}
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
removeCriteria={() => removeCriteria(index)}
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
/>
{/*JSON.stringify(criteria)*/}
</Box>
))
}
<Box p={1}>
<Button onClick={() => addCriteria()} startIcon={<Icon>add</Icon>} size="medium" sx={{px: 0.75}}>Add Condition</Button>
</Box>
</Box>
);
}
);

View File

@ -0,0 +1,418 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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;
onSave: (newValues: any[]) => void;
}
FilterCriteriaPaster.defaultProps = {};
function FilterCriteriaPaster({type, onSave}: 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 clearData = () =>
{
setDelimiter("");
setDelimiterCharacter("");
setChipData([]);
setInputText("");
setDetectedText("");
setCustomDelimiterValue("");
setPasteModalIsOpen(false);
};
const handleCancelClicked = () =>
{
clearData();
setPasteModalIsOpen(false);
};
const handleSaveClicked = () =>
{
////////////////////////////////////////
// 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]);
}
}
onSave(saveData);
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>
<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={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
</Tooltip>
{
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>
);
}
export default FilterCriteriaPaster;

View File

@ -0,0 +1,550 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl/FormControl";
import Icon from "@mui/material/Icon/Icon";
import IconButton from "@mui/material/IconButton";
import MenuItem from "@mui/material/MenuItem";
import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import React, {ReactNode, SyntheticEvent, useState} from "react";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
export enum ValueMode
{
NONE = "NONE",
SINGLE = "SINGLE",
DOUBLE = "DOUBLE",
MULTI = "MULTI",
SINGLE_DATE = "SINGLE_DATE",
SINGLE_DATE_TIME = "SINGLE_DATE_TIME",
PVS_SINGLE = "PVS_SINGLE",
PVS_MULTI = "PVS_MULTI",
}
export interface OperatorOption
{
label: string;
value: QCriteriaOperator;
implicitValues?: [any];
valueMode: ValueMode;
}
export const getDefaultCriteriaValue = () => [""];
interface FilterCriteriaRowProps
{
id: number;
index: number;
tableMetaData: QTableMetaData;
metaData: QInstance;
criteria: QFilterCriteria;
booleanOperator: "AND" | "OR" | null;
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
removeCriteria: () => void;
updateBooleanOperator: (newValue: string) => void;
}
FilterCriteriaRow.defaultProps = {};
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean)
{
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
for (let i = 0; i < sortedFields.length; i++)
{
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
}
}
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
{
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
const [operatorInputValue, setOperatorInputValue] = useState("");
///////////////////////////////////////////////////////////////
// set up the array of options for the fields Autocomplete //
// also, a groupBy function, in case there are exposed joins //
///////////////////////////////////////////////////////////////
const fieldOptions: any[] = [];
makeFieldOptionsForTable(tableMetaData, fieldOptions, false);
let fieldsGroupBy = null;
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
{
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const exposedJoin = tableMetaData.exposedJoins[i];
if (metaData.tables.has(exposedJoin.joinTable.name))
{
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
}
}
}
////////////////////////////////////////////////////////////
// set up array of options for operator dropdown //
// only call the function to do it if we have a field set //
////////////////////////////////////////////////////////////
let operatorOptions: OperatorOption[] = [];
function setOperatorOptions(fieldName: string)
{
const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
operatorOptions = [];
if (field && fieldTable)
{
//////////////////////////////////////////////////////
// setup array of options for operator Autocomplete //
//////////////////////////////////////////////////////
if (field.possibleValueSourceName)
{
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.PVS_MULTI});
}
else
{
switch (field.type)
{
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than or equals", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
break;
case QFieldType.DATE:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
//? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN});
//? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN});
break;
case QFieldType.DATE_TIME:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
break;
case QFieldType.BOOLEAN:
operatorOptions.push({label: "equals yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
operatorOptions.push({label: "equals no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
/*
? is yes or empty (is not no)
? is no or empty (is not yes)
*/
break;
case QFieldType.BLOB:
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
break;
default:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "contains ", value: QCriteriaOperator.CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not contain", value: QCriteriaOperator.NOT_CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "starts with", value: QCriteriaOperator.STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not start with", value: QCriteriaOperator.NOT_STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "ends with", value: QCriteriaOperator.ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not end with", value: QCriteriaOperator.NOT_ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
}
}
}
}
////////////////////////////////////////////////////////////////
// make currently selected values appear in the Autocompletes //
////////////////////////////////////////////////////////////////
let defaultFieldValue;
let field = null;
let fieldTable = null;
if(criteria && criteria.fieldName)
{
[field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName);
if (field && fieldTable)
{
if (fieldTable.name == tableMetaData.name)
{
// @ts-ignore
defaultFieldValue = {field: field, table: tableMetaData, fieldName: criteria.fieldName};
}
else
{
defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName};
}
setOperatorOptions(criteria.fieldName);
let newOperatorSelectedValue = operatorOptions.filter(option =>
{
if(option.value == criteria.operator)
{
if(option.implicitValues)
{
return (JSON.stringify(option.implicitValues) == JSON.stringify(criteria.values));
}
else
{
return (true);
}
}
return (false);
})[0];
if(newOperatorSelectedValue?.label !== operatorSelectedValue?.label)
{
setOperatorSelectedValue(newOperatorSelectedValue);
setOperatorInputValue(newOperatorSelectedValue?.label);
}
}
}
//////////////////////////////////////////////
// event handler for booleanOperator Select //
//////////////////////////////////////////////
const handleBooleanOperatorChange = (event: SelectChangeEvent<"AND" | "OR">, child: ReactNode) =>
{
updateBooleanOperator(event.target.value);
};
//////////////////////////////////////////
// event handler for field Autocomplete //
//////////////////////////////////////////
const handleFieldChange = (event: any, newValue: any, reason: string) =>
{
const oldFieldName = criteria.fieldName;
criteria.fieldName = newValue ? newValue.fieldName : null;
//////////////////////////////////////////////////////
// decide if we should clear out the values or not. //
//////////////////////////////////////////////////////
if (criteria.fieldName == null || isFieldTypeDifferent(oldFieldName, criteria.fieldName))
{
criteria.values = getDefaultCriteriaValue();
}
////////////////////////////////////////////////////////////////////
// update the operator options, and the operator on this criteria //
////////////////////////////////////////////////////////////////////
setOperatorOptions(criteria.fieldName);
if (operatorOptions.length)
{
if (isFieldTypeDifferent(oldFieldName, criteria.fieldName))
{
criteria.operator = operatorOptions[0].value;
setOperatorSelectedValue(operatorOptions[0]);
setOperatorInputValue(operatorOptions[0].label);
}
}
else
{
criteria.operator = null;
setOperatorSelectedValue(null);
setOperatorInputValue("");
}
updateCriteria(criteria, false);
};
/////////////////////////////////////////////
// event handler for operator Autocomplete //
/////////////////////////////////////////////
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
{
criteria.operator = newValue ? newValue.value : null;
if(newValue)
{
setOperatorSelectedValue(newValue);
setOperatorInputValue(newValue.label);
if(newValue.implicitValues)
{
criteria.values = newValue.implicitValues;
}
}
else
{
setOperatorSelectedValue(null);
setOperatorInputValue("");
}
updateCriteria(criteria, false);
};
//////////////////////////////////////////////////
// event handler for value field (of all types) //
//////////////////////////////////////////////////
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
{
// @ts-ignore
const value = newValue !== undefined ? newValue : event ? event.target.value : null;
if(!criteria.values)
{
criteria.values = [];
}
if(valueIndex == "all")
{
criteria.values = value;
}
else
{
criteria.values[valueIndex] = value;
}
updateCriteria(criteria, true);
};
const isFieldTypeDifferent = (fieldNameA: string, fieldNameB: string): boolean =>
{
const [fieldA] = FilterUtils.getField(tableMetaData, fieldNameA);
const [fieldB] = FilterUtils.getField(tableMetaData, fieldNameB);
if (fieldA?.type !== fieldB.type)
{
return (true);
}
if (fieldA.possibleValueSourceName !== fieldB.possibleValueSourceName)
{
return (true);
}
return (false);
};
function isFieldOptionEqual(option: any, value: any)
{
return option.fieldName === value.fieldName;
}
function getFieldOptionLabel(option: any)
{
/////////////////////////////////////////////////////////////////////////////////////////
// note - we're using renderFieldOption below for the actual select-box options, which //
// are always jut field label (as they are under groupings that show their table name) //
/////////////////////////////////////////////////////////////////////////////////////////
if(option && option.field && option.table)
{
if(option.table.name == tableMetaData.name)
{
return (option.field.label);
}
else
{
return (option.table.label + ": " + option.field.label);
}
}
return ("");
}
//////////////////////////////////////////////////////////////////////////////////////////////
// for options, we only want the field label (contrast with what we show in the input box, //
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
//////////////////////////////////////////////////////////////////////////////////////////////
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
{
let label = ""
if(option && option.field)
{
label = (option.field.label);
}
return (<li {...props}>{label}</li>);
}
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
{
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
}
let criteriaIsValid = true;
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
function isNotSet(value: any)
{
return (value === null || value == undefined || String(value).trim() === "");
}
if(!criteria.fieldName)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
}
else if(!criteria.operator)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
}
else
{
if(operatorSelectedValue)
{
if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues)
{
//////////////////////////////////
// don't need to look at values //
//////////////////////////////////
}
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE)
{
if(criteria.values.length < 2)
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter two values to complete the definition of this condition.";
}
}
else if(operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI)
{
if(criteria.values.length < 1 || isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
}
}
else
{
if(isNotSet(criteria.values[0]))
{
criteriaIsValid = false;
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
}
}
}
}
return (
<Box className="filterCriteriaRow" pt={0.5} display="flex" alignItems="flex-end">
<Box display="inline-block">
<Tooltip title="Remove this condition from your filter" enterDelay={750} placement="left">
<IconButton onClick={removeCriteria}><Icon fontSize="small">close</Icon></IconButton>
</Tooltip>
</Box>
<Box display="inline-block" width={55} className="booleanOperatorColumn">
{booleanOperator && index > 0 ?
<FormControl variant="standard" sx={{verticalAlign: "bottom"}} fullWidth>
<Select value={booleanOperator} disabled={index > 1} onChange={handleBooleanOperatorChange}>
<MenuItem value="AND">And</MenuItem>
<MenuItem value="OR">Or</MenuItem>
</Select>
</FormControl>
: <span />}
</Box>
<Box display="inline-block" width={250} className="fieldColumn">
<Autocomplete
id={`field-${id}`}
renderInput={(params) => (<TextField {...params} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
// @ts-ignore
defaultValue={defaultFieldValue}
options={fieldOptions}
onChange={handleFieldChange}
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
groupBy={fieldsGroupBy}
getOptionLabel={(option) => getFieldOptionLabel(option)}
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
/>
</Box>
<Box display="inline-block" width={200} className="operatorColumn">
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={750}>
<Autocomplete
id={"criteriaOperator"}
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
options={operatorOptions}
value={operatorSelectedValue as any}
inputValue={operatorInputValue}
onChange={handleOperatorChange}
onInputChange={(e, value) => setOperatorInputValue(value)}
isOptionEqualToValue={(option, value) => isOperatorOptionEqual(option, value)}
getOptionLabel={(option: any) => option.label}
autoSelect={true}
autoHighlight={true}
slotProps={{popper: {style: {padding: 0, maxHeight: "unset", width: "200px"}}}}
/*disabled={criteria.fieldName == null}*/
/>
</Tooltip>
</Box>
<Box display="inline-block" width={300} className="filterValuesColumn">
<FilterCriteriaRowValues
operatorOption={operatorSelectedValue}
criteria={{id: id, ...criteria}}
field={field}
table={fieldTable}
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
/>
</Box>
<Box display="inline-block" pl={0.5} pr={1}>
<Tooltip title={criteriaStatusTooltip} enterDelay={750} placement="right">
{
criteriaIsValid
? <Icon color="success">check</Icon>
: <Icon color="disabled">pending</Icon>
}
</Tooltip>
</Box>
</Box>
);
}

View File

@ -0,0 +1,242 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
import TextField from "@mui/material/TextField";
import React, {SyntheticEvent, useReducer} from "react";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props
{
operatorOption: OperatorOption;
criteria: QFilterCriteriaWithId;
field: QFieldMetaData;
table: QTableMetaData;
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
}
FilterCriteriaRowValues.defaultProps = {
};
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!operatorOption)
{
return <br />;
}
const getTypeForTextField = (): string =>
{
let type = "search";
if (field.type == QFieldType.INTEGER)
{
type = "number";
}
else if (field.type == QFieldType.DATE)
{
type = "date";
}
else if (field.type == QFieldType.DATE_TIME)
{
type = "datetime-local";
}
return (type);
};
const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
{
let type = getTypeForTextField();
const inputLabelProps: any = {};
if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME)
{
inputLabelProps.shrink = true;
}
let value = criteria.values[valueIndex];
if (field.type == QFieldType.DATE_TIME && value && String(value).indexOf("Z") > -1)
{
value = ValueUtils.formatDateTimeValueForForm(value);
}
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{
valueChangeHandler(event, index, "");
document.getElementById(`${idPrefix}${criteria.id}`).focus();
};
const inputProps: any = {};
inputProps.endAdornment = (
<InputAdornment position="end">
<IconButton sx={{visibility: value ? "visible" : "hidden"}} onClick={(event) => clearValue(event, valueIndex)}>
<Icon>close</Icon>
</IconButton>
</InputAdornment>
);
return <TextField
id={`${idPrefix}${criteria.id}`}
label={label}
variant="standard"
autoComplete="off"
type={type}
onChange={(event) => valueChangeHandler(event, valueIndex)}
value={value}
InputLabelProps={inputLabelProps}
InputProps={inputProps}
fullWidth
/>;
};
function saveNewPasterValues(newValues: any[])
{
if (criteria.values)
{
criteria.values = [...criteria.values, ...newValues];
}
else
{
criteria.values = newValues;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we are somehow getting some empty-strings as first-value leaking through. they aren't cool, so, remove them if we find them //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (criteria.values.length > 0 && criteria.values[0] == "")
{
criteria.values = criteria.values.splice(1);
}
valueChangeHandler(null, "all", criteria.values);
forceUpdate();
}
switch (operatorOption.valueMode)
{
case ValueMode.NONE:
return <br />;
case ValueMode.SINGLE:
return makeTextField();
case ValueMode.SINGLE_DATE:
return makeTextField();
case ValueMode.SINGLE_DATE_TIME:
return makeTextField();
case ValueMode.DOUBLE:
return <Box>
<Box width="50%" display="inline-block">
{ makeTextField(0, "From", "from-") }
</Box>
<Box width="50%" display="inline-block">
{makeTextField(1, "To", "to-")}
</Box>
</Box>;
case ValueMode.MULTI:
let values = criteria.values;
if (values && values.length == 1 && values[0] == "")
{
values = [];
}
return <Box display="flex" alignItems="flex-end" className="multiValue">
<Autocomplete
renderInput={(params) => (<TextField {...params} variant="standard" label="Values" />)}
options={[]}
multiple
freeSolo // todo - no debounce after enter?
selectOnFocus
clearOnBlur
fullWidth
limitTags={5}
value={values}
onChange={(event, value) => valueChangeHandler(event, "all", value)}
/>
<Box>
<FilterCriteriaPaster type={getTypeForTextField()} onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
</Box>
</Box>;
case ValueMode.PVS_SINGLE:
console.log("Doing pvs single: " + criteria.values);
let selectedPossibleValue = null;
if (criteria.values && criteria.values.length > 0)
{
selectedPossibleValue = criteria.values[0];
}
return <Box mb={-1.5}>
<DynamicSelect
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-single-" + criteria.id}
key={field.name + "-single-" + criteria.id}
fieldLabel="Value"
initialValue={selectedPossibleValue?.id}
initialDisplayValue={selectedPossibleValue?.label}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)}
/>
</Box>;
case ValueMode.PVS_MULTI:
console.log("Doing pvs multi: " + criteria.values);
let initialValues: any[] = [];
if (criteria.values && criteria.values.length > 0)
{
if (criteria.values.length == 1 && criteria.values[0] == "")
{
// we never want a tag that's just ""...
}
else
{
initialValues = criteria.values;
}
}
return <Box mb={-1.5}>
<DynamicSelect
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-multi-" + criteria.id}
key={field.name + "-multi-" + criteria.id}
isMultiple
fieldLabel="Values"
initialValues={initialValues}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
/>
</Box>
}
return (<br />);
}
export default FilterCriteriaRowValues;

View File

@ -101,6 +101,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
completions.push({value: "api.bulkInsert(", meta: "Create multiple records in a table."});
completions.push({value: "api.bulkUpdate(", meta: "Update multiple records in a table."});
completions.push({value: "api.bulkDelete(", meta: "Remove multiple records from a table."});
completions.push({value: "api.runProcess(", meta: "Run a process"});
// completions.push({value: "api.newRecord(", meta: "Create a new QRecord object."});
// completions.push({value: "api.newQueryInput(", meta: "Create a new QueryInput object."});
// completions.push({value: "api.newQueryFilter(", meta: "Create a new QueryFilter object."});

View File

@ -30,8 +30,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
import Typography from "@mui/material/Typography";
import parse from "html-react-parser";
import React, {useEffect, useState} from "react";
import {Link, useNavigate, NavigateFunction} from "react-router-dom";
import {bool} from "yup";
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
import colors from "qqq/components/legacy/colors";
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
@ -94,7 +93,9 @@ export class LabelComponent
}
/*******************************************************************************
**
*******************************************************************************/
export class HeaderLink extends LabelComponent
{
label: string;
@ -118,7 +119,9 @@ export class HeaderLink extends LabelComponent
}
/*******************************************************************************
**
*******************************************************************************/
export class AddNewRecordButton extends LabelComponent
{
table: QTableMetaData;
@ -152,6 +155,9 @@ export class AddNewRecordButton extends LabelComponent
}
/*******************************************************************************
**
*******************************************************************************/
export class ExportDataButton extends LabelComponent
{
callbackToExport: any;
@ -177,26 +183,30 @@ export class ExportDataButton extends LabelComponent
}
/*******************************************************************************
**
*******************************************************************************/
export class Dropdown extends LabelComponent
{
label: string;
options: DropdownOption[];
onChangeCallback: any
dropdownName: string;
onChangeCallback: any;
constructor(label: string, options: DropdownOption[], onChangeCallback: any)
constructor(label: string, options: DropdownOption[], dropdownName: string, onChangeCallback: any)
{
super();
this.label = label;
this.options = options;
this.dropdownName = dropdownName;
this.onChangeCallback = onChangeCallback;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
let defaultValue = null;
const dropdownName = args.widgetProps.widgetData.dropdownNameList[args.componentIndex];
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${dropdownName}`;
if(args.widgetProps.storeDropdownSelections)
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
if (args.widgetProps.storeDropdownSelections)
{
///////////////////////////////////////////////////////////////////////////////////////
// see if an existing value is stored in local storage, and if so set it in dropdown //
@ -208,7 +218,7 @@ export class Dropdown extends LabelComponent
return (
<Box my={2} sx={{float: "right"}}>
<DropdownMenu
name={dropdownName}
name={this.dropdownName}
defaultValue={defaultValue}
sx={{width: 200, marginLeft: "15px"}}
label={`Select ${this.label}`}
@ -221,6 +231,9 @@ export class Dropdown extends LabelComponent
}
/*******************************************************************************
**
*******************************************************************************/
export class ReloadControl extends LabelComponent
{
callback: () => void;
@ -235,7 +248,7 @@ export class ReloadControl extends LabelComponent
{
return (
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
<Tooltip title="Refresh"><Button sx={{px: 1, py:0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
</Typography>
);
}
@ -245,59 +258,101 @@ export class ReloadControl extends LabelComponent
export const WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT = "qqq.widgets.dropdownData";
/*******************************************************************************
**
*******************************************************************************/
function Widget(props: React.PropsWithChildren<Props>): JSX.Element
{
const navigate = useNavigate();
const [dropdownData, setDropdownData] = useState([]);
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
const [reloading, setReloading] = useState(false);
const [dropdownDataJSON, setDropdownDataJSON] = useState("");
const [labelComponentsLeft, setLabelComponentsLeft] = useState([] as LabelComponent[]);
const [labelComponentsRight, setLabelComponentsRight] = useState([] as LabelComponent[]);
function renderComponent(component: LabelComponent, componentIndex: number)
{
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload})
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
}
///////////////////////////////////////////////////////////////////
// make dropdowns from the widgetData appear as label-components //
///////////////////////////////////////////////////////////////////
const effectiveLabelAdditionalComponentsRight: LabelComponent[] = [];
if(props.labelAdditionalComponentsRight)
useEffect(() =>
{
props.labelAdditionalComponentsRight.map((component) => effectiveLabelAdditionalComponentsRight.push(component));
}
if(props.widgetData && props.widgetData.dropdownDataList)
{
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
////////////////////////////////////////////////////////////////////////////////
// for initial render, put left-components from props into the state variable //
// plus others we can infer from other props //
////////////////////////////////////////////////////////////////////////////////
const stateLabelComponentsLeft: LabelComponent[] = [];
if (props.reloadWidgetCallback && props.widgetData && props.showReloadControl && props.widgetMetaData.showReloadButton)
{
effectiveLabelAdditionalComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, handleDataChange))
});
stateLabelComponentsLeft.push(new ReloadControl(doReload));
}
if (props.labelAdditionalComponentsLeft)
{
props.labelAdditionalComponentsLeft.map((component) => stateLabelComponentsLeft.push(component));
}
setLabelComponentsLeft(stateLabelComponentsLeft);
}, []);
useEffect(() =>
{
/////////////////////////////////////////////////////////////////////////////////
// for initial render, put right-components from props into the state variable //
/////////////////////////////////////////////////////////////////////////////////
const stateLabelComponentsRight = [] as LabelComponent[];
// console.log(`${props.widgetMetaData.name} init'ing right-components`);
if (props.labelAdditionalComponentsRight)
{
props.labelAdditionalComponentsRight.map((component) => stateLabelComponentsRight.push(component));
}
setLabelComponentsRight(stateLabelComponentsRight);
}, []);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have widgetData, and it has a dropdown list, capture that in a state variable, if it's changed //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
if (props.widgetData && props.widgetData.dropdownDataList)
{
const currentDropdownDataJSON = JSON.stringify(props.widgetData.dropdownDataList);
if (currentDropdownDataJSON !== dropdownDataJSON)
{
// console.log(`${props.widgetMetaData.name} we have (new) dropdown data!!: ${currentDropdownDataJSON}`);
setDropdownDataJSON(currentDropdownDataJSON);
}
}
useEffect(() =>
{
///////////////////////////////////////////////////////////////////////////////////
// if we've seen a change in the dropdown data, then update the right-components //
///////////////////////////////////////////////////////////////////////////////////
// console.log(`${props.widgetMetaData.name} in useEffect post dropdownData change`);
if (props.widgetData && props.widgetData.dropdownDataList)
{
const updatedStateLabelComponentsRight = JSON.parse(JSON.stringify(labelComponentsRight)) as LabelComponent[];
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
{
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, props.widgetData.dropdownNameList[index], handleDataChange));
});
setLabelComponentsRight(updatedStateLabelComponentsRight);
}
}, [dropdownDataJSON]);
const doReload = () =>
{
setReloading(true);
reloadWidget(dropdownData);
}
};
useEffect(() =>
{
setReloading(false);
}, [props.widgetData]);
const effectiveLabelAdditionalComponentsLeft: LabelComponent[] = [];
if(props.reloadWidgetCallback && props.widgetData && props.showReloadControl && props.widgetMetaData.showReloadButton)
{
effectiveLabelAdditionalComponentsLeft.push(new ReloadControl(doReload))
}
if(props.labelAdditionalComponentsLeft)
{
props.labelAdditionalComponentsLeft.map((component) => effectiveLabelAdditionalComponentsLeft.push(component));
}
function handleDataChange(dropdownLabel: string, changedData: any)
{
if(dropdownData)
if (dropdownData)
{
///////////////////////////////////////////
// find the index base on selected label //
@ -327,7 +382,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
// if should store in local storage, do so now //
// or remove if dropdown was cleared out //
/////////////////////////////////////////////////
if(props.storeDropdownSelections)
if (props.storeDropdownSelections)
{
if (changedData?.id)
{
@ -371,7 +426,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
const toggleFullScreenWidget = () =>
{
if(fullScreenWidgetClassName)
if (fullScreenWidgetClassName)
{
setFullScreenWidgetClassName("");
}
@ -385,17 +440,17 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
const isSet = (v: any): boolean =>
{
return(v !== null && v !== undefined);
return (v !== null && v !== undefined);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let needLabelBox = false;
if(hasPermission)
if (hasPermission)
{
needLabelBox ||= (effectiveLabelAdditionalComponentsLeft && effectiveLabelAdditionalComponentsLeft.length > 0);
needLabelBox ||= (effectiveLabelAdditionalComponentsRight && effectiveLabelAdditionalComponentsRight.length > 0);
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
needLabelBox ||= isSet(props.widgetMetaData?.icon);
needLabelBox ||= isSet(props.widgetData?.label);
needLabelBox ||= isSet(props.widgetMetaData?.label);
@ -406,90 +461,90 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
{
needLabelBox &&
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
<Box pt={2} pb={1}>
{
hasPermission ?
props.widgetMetaData?.icon && (
<Box
ml={3}
mt={-4}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "64px",
height: "64px",
borderRadius: "8px",
background: colors.info.main,
color: "#ffffff",
float: "left"
}}
>
<Icon fontSize="medium" color="inherit">
{props.widgetMetaData.icon}
</Icon>
</Box>
) : (
<Box
ml={3}
mt={-4}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "64px",
height: "64px",
borderRadius: "8px",
background: colors.info.main,
color: "#ffffff",
float: "left"
}}
>
<Icon fontSize="medium" color="inherit">lock</Icon>
</Box>
)
}
{
//////////////////////////////////////////////////////////////////////////////////////////
// first look for a label in the widget data, which would override that in the metadata //
//////////////////////////////////////////////////////////////////////////////////////////
hasPermission && props.widgetData?.label? (
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={2} display="inline-block">
{props.widgetData.label}
</Typography>
) : (
hasPermission && props.widgetMetaData?.label && (
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={3} display="inline-block">
{props.widgetMetaData.label}
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
<Box pt={2} pb={1}>
{
hasPermission ?
props.widgetMetaData?.icon && (
<Box
ml={3}
mt={-4}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "64px",
height: "64px",
borderRadius: "8px",
background: colors.info.main,
color: "#ffffff",
float: "left"
}}
>
<Icon fontSize="medium" color="inherit">
{props.widgetMetaData.icon}
</Icon>
</Box>
) :
(
<Box
ml={3}
mt={-4}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "64px",
height: "64px",
borderRadius: "8px",
background: colors.info.main,
color: "#ffffff",
float: "left"
}}
>
<Icon fontSize="medium" color="inherit">lock</Icon>
</Box>
)
}
{
//////////////////////////////////////////////////////////////////////////////////////////
// first look for a label in the widget data, which would override that in the metadata //
//////////////////////////////////////////////////////////////////////////////////////////
hasPermission && props.widgetData?.label ? (
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={2} display="inline-block">
{props.widgetData.label}
</Typography>
) : (
hasPermission && props.widgetMetaData?.label && (
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={3} display="inline-block">
{props.widgetMetaData.label}
</Typography>
)
)
)
}
{
hasPermission && (
effectiveLabelAdditionalComponentsLeft.map((component, i) =>
{
return (<span key={i}>{renderComponent(component, i)}</span>);
})
)
}
}
{
hasPermission && (
labelComponentsLeft.map((component, i) =>
{
return (<span key={i}>{renderComponent(component, i)}</span>);
})
)
}
</Box>
<Box>
{
hasPermission && (
labelComponentsRight.map((component, i) =>
{
return (<span key={i}>{renderComponent(component, i)}</span>);
})
)
}
</Box>
</Box>
<Box>
{
hasPermission && (
effectiveLabelAdditionalComponentsRight.map((component, i) =>
{
return (<span key={i}>{renderComponent(component, i)}</span>);
})
)
}
</Box>
</Box>
}
{
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem"/>)
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem" />)
}
{
errorLoading ? (
@ -514,7 +569,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
)
}
{
! errorLoading && props?.footerHTML && (
!errorLoading && props?.footerHTML && (
<Box mt={1} ml={3} mr={3} mb={2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
)
}