mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
Merge branch 'integration/sprint-28' into feature/CTLE-214-dot-menu
This commit is contained in:
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
192
src/qqq/components/query/CustomFilterPanel.tsx
Normal file
192
src/qqq/components/query/CustomFilterPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
418
src/qqq/components/query/FilterCriteriaPaster.tsx
Normal file
418
src/qqq/components/query/FilterCriteriaPaster.tsx
Normal 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;
|
550
src/qqq/components/query/FilterCriteriaRow.tsx
Normal file
550
src/qqq/components/query/FilterCriteriaRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
242
src/qqq/components/query/FilterCriteriaRowValues.tsx
Normal file
242
src/qqq/components/query/FilterCriteriaRowValues.tsx
Normal 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;
|
@ -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."});
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user