mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Adding possible-value dropdowns to forms and filters
This commit is contained in:
@ -13,13 +13,13 @@
|
||||
"@fullcalendar/interaction": "5.10.0",
|
||||
"@fullcalendar/react": "5.10.0",
|
||||
"@fullcalendar/timegrid": "5.10.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.24",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.25",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.4.1",
|
||||
"@mui/styled-engine": "5.4.1",
|
||||
"@mui/styles": "5.10.7",
|
||||
"@mui/x-data-grid": "5.13.0",
|
||||
"@mui/x-data-grid-pro": "5.13.0",
|
||||
"@mui/x-data-grid": "5.17.6",
|
||||
"@mui/x-data-grid-pro": "5.17.6",
|
||||
"@mui/x-license-pro": "5.12.3",
|
||||
"@react-jvectormap/core": "1.0.1",
|
||||
"@react-jvectormap/unitedstates": "1.0.1",
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
@ -42,6 +43,7 @@ import MDBox from "qqq/components/Temporary/MDBox";
|
||||
import MDTypography from "qqq/components/Temporary/MDTypography";
|
||||
import QClient from "qqq/utils/QClient";
|
||||
import QTableUtils from "qqq/utils/QTableUtils";
|
||||
import QValueUtils from "qqq/utils/QValueUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -120,9 +122,10 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// if doing an edit, fetch the record and pre-populate the form values from it //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
let record: QRecord = null;
|
||||
if (id !== null)
|
||||
{
|
||||
const record = await qController.get(tableName, id);
|
||||
record = await qController.get(tableName, id);
|
||||
setRecord(record);
|
||||
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||
setPageHeader(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||
@ -130,6 +133,10 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
||||
{
|
||||
initialValues[key] = record.values.get(key);
|
||||
if(fieldMetaData.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
initialValues[key] = QValueUtils.formatDateTimeValueForForm(record.values.get(key));
|
||||
}
|
||||
});
|
||||
|
||||
setFormValues(formValues);
|
||||
@ -173,6 +180,24 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
{
|
||||
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// add props for possible value fields //
|
||||
/////////////////////////////////////////
|
||||
if(field.possibleValueSourceName)
|
||||
{
|
||||
let initialDisplayValue = null;
|
||||
if(record && record.displayValues)
|
||||
{
|
||||
initialDisplayValue = record.displayValues.get(field.name);
|
||||
}
|
||||
dynamicFormFields[fieldName].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
tableName: tableName,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionDynamicFormFields.length === 0)
|
||||
@ -245,7 +270,9 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
setAlertContent(error.response.data.error);
|
||||
console.log("Caught:");
|
||||
console.log(error);
|
||||
setAlertContent(error.message);
|
||||
});
|
||||
}
|
||||
else
|
||||
@ -259,7 +286,7 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
setAlertContent(error.response.data.error);
|
||||
setAlertContent(error.message);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@ -298,7 +325,7 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
<Form id={formId} autoComplete="off">
|
||||
|
||||
<MDBox pb={3} pt={0}>
|
||||
<Card id={`${t1sectionName}`} sx={{overflow: "visible"}}>
|
||||
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}}>
|
||||
<MDBox display="flex" p={3} pb={1}>
|
||||
<MDBox mr={1.5}>
|
||||
<Avatar sx={{bgcolor: colors.info.main}}>
|
||||
@ -313,7 +340,7 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
</MDBox>
|
||||
{
|
||||
t1sectionName && formFields ? (
|
||||
<MDBox pb={3} px={3}>
|
||||
<MDBox pb={1} px={3}>
|
||||
<MDBox p={3} width="100%">
|
||||
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
|
||||
</MDBox>
|
||||
@ -324,11 +351,11 @@ function EntityForm({table, id}: Props): JSX.Element
|
||||
</MDBox>
|
||||
{formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
|
||||
<MDBox key={`edit-card-${section.name}`} pb={3}>
|
||||
<Card id={section.name} sx={{overflow: "visible"}}>
|
||||
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}}>
|
||||
<MDTypography variant="h5" p={3} pb={1}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
<MDBox pb={3} px={3}>
|
||||
<MDBox pb={1} px={3}>
|
||||
<MDBox p={3} width="100%">
|
||||
{
|
||||
getFormSection(values, touched, formFields.get(section.name), errors)
|
||||
|
@ -19,13 +19,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {colors} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {useFormikContext} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import QDynamicFormField from "qqq/components/QDynamicFormField";
|
||||
import QDynamicSelect from "qqq/components/QDynamicSelect/QDynamicSelect";
|
||||
import MDBox from "qqq/components/Temporary/MDBox";
|
||||
import MDTypography from "qqq/components/Temporary/MDTypography";
|
||||
|
||||
@ -124,6 +124,22 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
);
|
||||
}
|
||||
|
||||
// possible values!!
|
||||
if (field.possibleValueProps)
|
||||
{
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
<QDynamicSelect
|
||||
tableName={field.possibleValueProps.tableName}
|
||||
fieldName={fieldName}
|
||||
fieldLabel={field.label}
|
||||
initialValue={values[fieldName]}
|
||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// todo? inputProps={{ autoComplete: "" }}
|
||||
// todo? placeholder={password.placeholder}
|
||||
return (
|
||||
|
@ -87,7 +87,15 @@ function QDynamicFormField({
|
||||
(type == "checkbox" ?
|
||||
<QBooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} /> :
|
||||
<>
|
||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled} />
|
||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
onKeyPress={(e: any) =>
|
||||
{
|
||||
if(e.key === "Enter")
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MDBox mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} /></div>}
|
||||
|
215
src/qqq/components/QDynamicSelect/QDynamicSelect.tsx
Normal file
215
src/qqq/components/QDynamicSelect/QDynamicSelect.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {CircularProgress, FilterOptionsState} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useFormikContext} from "formik";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import QClient from "qqq/utils/QClient";
|
||||
|
||||
interface Props
|
||||
{
|
||||
tableName: string;
|
||||
fieldName: string;
|
||||
fieldLabel: string;
|
||||
inForm: boolean;
|
||||
initialValue?: any;
|
||||
initialDisplayValue?: string;
|
||||
onChange?: any
|
||||
}
|
||||
|
||||
QDynamicSelect.defaultProps = {
|
||||
inForm: true,
|
||||
initialValue: null,
|
||||
initialDisplayValue: null,
|
||||
onChange: null,
|
||||
};
|
||||
|
||||
const qController = QClient.getInstance();
|
||||
|
||||
function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, onChange}: Props)
|
||||
{
|
||||
const [ open, setOpen ] = useState(false);
|
||||
const [ options, setOptions ] = useState<readonly QPossibleValue[]>([]);
|
||||
const [ searchTerm, setSearchTerm ] = useState(null);
|
||||
const [ firstRender, setFirstRender ] = useState(true);
|
||||
const [defaultValue, _] = useState(initialValue && initialDisplayValue ? {id: initialValue, label: initialDisplayValue} : null);
|
||||
// const loading = open && options.length === 0;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null;
|
||||
if(inForm)
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
setFieldValueRef = setFieldValue;
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(firstRender)
|
||||
{
|
||||
// console.log("First render, so not searching...");
|
||||
setFirstRender(false);
|
||||
return;
|
||||
}
|
||||
// console.log("Use effect for searchTerm - searching!");
|
||||
|
||||
let active = true;
|
||||
|
||||
setLoading(true);
|
||||
(async () =>
|
||||
{
|
||||
// console.log(`doing a search with ${searchTerm}`);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, fieldName, searchTerm ?? "");
|
||||
setLoading(false);
|
||||
// console.log("Results:")
|
||||
// console.log(`${results}`);
|
||||
if (active)
|
||||
{
|
||||
setOptions([ ...results ]);
|
||||
}
|
||||
})();
|
||||
|
||||
return () =>
|
||||
{
|
||||
active = false;
|
||||
};
|
||||
}, [ searchTerm ]);
|
||||
|
||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||
{
|
||||
console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||
if(reason !== "reset")
|
||||
{
|
||||
// console.log(` -> setting search term to ${value}`);
|
||||
setSearchTerm(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (x: any) =>
|
||||
{
|
||||
setSearchTerm(null);
|
||||
}
|
||||
|
||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||
{
|
||||
// console.log("handleChanged. value is:");
|
||||
// console.log(value);
|
||||
setSearchTerm(null);
|
||||
|
||||
if(onChange)
|
||||
{
|
||||
onChange(value ? new QPossibleValue(value) : null);
|
||||
}
|
||||
else if(setFieldValueRef)
|
||||
{
|
||||
setFieldValueRef(fieldName, value ? value.id : null);
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// this looks like a no-op, but it's important to have, otherwise, we can only //
|
||||
// get options whose text/label matches the input (e.g., not ids that match) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
return (options);
|
||||
}
|
||||
|
||||
const renderOption = (props: Object, option: any) =>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we provide a custom renderOption method, to prevent a bug we saw during development, //
|
||||
// where if multiple options had an identical label, then the widget would ... i don't know, //
|
||||
// show more options than it should - it was odd to see, and it could be fixed by changing //
|
||||
// a PVS's format to include id - so the idea came, that maybe the LI's needed unique key //
|
||||
// attributes. so, doing this, w/ key=id, seemed to fix it. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (
|
||||
<li {...props} key={option.id}>
|
||||
{option.label}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
id={fieldName}
|
||||
open={open}
|
||||
fullWidth
|
||||
onOpen={() =>
|
||||
{
|
||||
setOpen(true);
|
||||
// console.log("setting open...");
|
||||
if(options.length == 0)
|
||||
{
|
||||
// console.log("no options yet, so setting search term to ''...");
|
||||
setSearchTerm("");
|
||||
}
|
||||
}}
|
||||
onClose={() =>
|
||||
{
|
||||
setOpen(false);
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option) => option.label}
|
||||
options={options}
|
||||
loading={loading}
|
||||
onInputChange={inputChanged}
|
||||
onBlur={handleBlur}
|
||||
defaultValue={defaultValue}
|
||||
// @ts-ignore
|
||||
onChange={handleChanged}
|
||||
noOptionsText={"No matches found"}
|
||||
onKeyPress={e =>
|
||||
{
|
||||
if (e.key === "Enter")
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
renderOption={renderOption}
|
||||
filterOptions={filterOptions}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={fieldLabel}
|
||||
variant="standard"
|
||||
autoComplete="off"
|
||||
type="search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
{loading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default QDynamicSelect;
|
@ -65,7 +65,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light}: Props): JSX.
|
||||
|
||||
|
||||
return (
|
||||
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: "1%"}}>
|
||||
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: "100px"}}>
|
||||
<MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
||||
{
|
||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
||||
|
320
src/qqq/pages/entity-list/QGridFilterOperators.tsx
Normal file
320
src/qqq/pages/entity-list/QGridFilterOperators.tsx
Normal file
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {TextFieldProps} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {getGridNumericOperators, getGridStringOperators, GridColDef, GridFilterInputValueProps, GridFilterItem} from "@mui/x-data-grid-pro";
|
||||
import {GridFilterInputValue} from "@mui/x-data-grid/components/panel/filterPanel/GridFilterInputValue";
|
||||
import {GridApiCommunity} from "@mui/x-data-grid/internals";
|
||||
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import QDynamicSelect from "qqq/components/QDynamicSelect/QDynamicSelect";
|
||||
|
||||
//////////////////////
|
||||
// string operators //
|
||||
//////////////////////
|
||||
const stringNotEqualsOperator: GridFilterOperator = {
|
||||
label: "does not equal",
|
||||
value: "isNot",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: GridFilterInputValue,
|
||||
};
|
||||
|
||||
const stringNotContainsOperator: GridFilterOperator = {
|
||||
label: "does not contain",
|
||||
value: "notContains",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: GridFilterInputValue,
|
||||
};
|
||||
|
||||
const stringNotStartsWithOperator: GridFilterOperator = {
|
||||
label: "does not start with",
|
||||
value: "notStartsWith",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: GridFilterInputValue,
|
||||
};
|
||||
|
||||
const stringNotEndWithOperator: GridFilterOperator = {
|
||||
label: "does not end with",
|
||||
value: "notEndsWith",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: GridFilterInputValue,
|
||||
};
|
||||
|
||||
let gridStringOperators = getGridStringOperators();
|
||||
let equals = gridStringOperators.splice(1, 1)[0];
|
||||
let contains = gridStringOperators.splice(0, 1)[0];
|
||||
let startsWith = gridStringOperators.splice(0, 1)[0];
|
||||
let endsWith = gridStringOperators.splice(0, 1)[0];
|
||||
gridStringOperators = [ equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators ];
|
||||
|
||||
export const QGridStringOperators = gridStringOperators;
|
||||
|
||||
|
||||
///////////////////////////////////////
|
||||
// input element for numbers-between //
|
||||
///////////////////////////////////////
|
||||
function InputNumberInterval(props: GridFilterInputValueProps)
|
||||
{
|
||||
const SUBMIT_FILTER_STROKE_TIME = 500;
|
||||
const {item, applyValue, focusElementRef = null} = props;
|
||||
|
||||
const filterTimeout = useRef<any>();
|
||||
const [ filterValueState, setFilterValueState ] = useState<[ string, string ]>(
|
||||
item.value ?? "",
|
||||
);
|
||||
const [ applying, setIsApplying ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
clearTimeout(filterTimeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const itemValue = item.value ?? [ undefined, undefined ];
|
||||
setFilterValueState(itemValue);
|
||||
}, [ item.value ]);
|
||||
|
||||
const updateFilterValue = (lowerBound: string, upperBound: string) =>
|
||||
{
|
||||
clearTimeout(filterTimeout.current);
|
||||
setFilterValueState([ lowerBound, upperBound ]);
|
||||
|
||||
setIsApplying(true);
|
||||
filterTimeout.current = setTimeout(() =>
|
||||
{
|
||||
setIsApplying(false);
|
||||
applyValue({...item, value: [ lowerBound, upperBound ]});
|
||||
}, SUBMIT_FILTER_STROKE_TIME);
|
||||
};
|
||||
|
||||
const handleUpperFilterChange: TextFieldProps["onChange"] = (event) =>
|
||||
{
|
||||
const newUpperBound = event.target.value;
|
||||
updateFilterValue(filterValueState[0], newUpperBound);
|
||||
};
|
||||
const handleLowerFilterChange: TextFieldProps["onChange"] = (event) =>
|
||||
{
|
||||
const newLowerBound = event.target.value;
|
||||
updateFilterValue(newLowerBound, filterValueState[1]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "end",
|
||||
height: 48,
|
||||
pl: "20px",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="lower-bound-input"
|
||||
placeholder="From"
|
||||
label="From"
|
||||
variant="standard"
|
||||
value={Number(filterValueState[0])}
|
||||
onChange={handleLowerFilterChange}
|
||||
type="number"
|
||||
inputRef={focusElementRef}
|
||||
sx={{mr: 2}}
|
||||
/>
|
||||
<TextField
|
||||
name="upper-bound-input"
|
||||
placeholder="To"
|
||||
label="To"
|
||||
variant="standard"
|
||||
value={Number(filterValueState[1])}
|
||||
onChange={handleUpperFilterChange}
|
||||
type="number"
|
||||
InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////
|
||||
// number operators //
|
||||
//////////////////////
|
||||
const betweenOperator: GridFilterOperator = {
|
||||
label: "is between",
|
||||
value: "between",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: InputNumberInterval
|
||||
};
|
||||
|
||||
const notBetweenOperator: GridFilterOperator = {
|
||||
label: "is not between",
|
||||
value: "notBetween",
|
||||
getApplyFilterFn: () => null,
|
||||
// @ts-ignore
|
||||
InputComponent: InputNumberInterval
|
||||
};
|
||||
|
||||
export const QGridNumericOperators = [ ...getGridNumericOperators(), betweenOperator, notBetweenOperator ];
|
||||
|
||||
|
||||
///////////////////////
|
||||
// boolean operators //
|
||||
///////////////////////
|
||||
const booleanTrueOperator: GridFilterOperator = {
|
||||
label: "is yes",
|
||||
value: "isTrue",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanFalseOperator: GridFilterOperator = {
|
||||
label: "is no",
|
||||
value: "isFalse",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanEmptyOperator: GridFilterOperator = {
|
||||
label: "is empty",
|
||||
value: "isEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanNotEmptyOperator: GridFilterOperator = {
|
||||
label: "is not empty",
|
||||
value: "isNotEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
export const QGridBooleanOperators = [ booleanTrueOperator, booleanFalseOperator, booleanEmptyOperator, booleanNotEmptyOperator ];
|
||||
|
||||
|
||||
///////////////////////////////////////
|
||||
// input element for possible values //
|
||||
///////////////////////////////////////
|
||||
function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData, props: GridFilterInputValueProps)
|
||||
{
|
||||
const SUBMIT_FILTER_STROKE_TIME = 500;
|
||||
const {item, applyValue, focusElementRef = null} = props;
|
||||
|
||||
console.log("Item.value? " + item.value);
|
||||
|
||||
const filterTimeout = useRef<any>();
|
||||
const [ filterValueState, setFilterValueState ] = useState<any>(item.value ?? null);
|
||||
const [ selectedPossibleValue, setSelectedPossibleValue ] = useState((item.value ?? null) as QPossibleValue);
|
||||
const [ applying, setIsApplying ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
clearTimeout(filterTimeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const itemValue = item.value ?? null;
|
||||
setFilterValueState(itemValue);
|
||||
}, [ item.value ]);
|
||||
|
||||
const updateFilterValue = (value: QPossibleValue) =>
|
||||
{
|
||||
clearTimeout(filterTimeout.current);
|
||||
setFilterValueState(value);
|
||||
|
||||
setIsApplying(true);
|
||||
filterTimeout.current = setTimeout(() =>
|
||||
{
|
||||
setIsApplying(false);
|
||||
applyValue({...item, value: value});
|
||||
}, SUBMIT_FILTER_STROKE_TIME);
|
||||
};
|
||||
|
||||
const handleChange = (value: QPossibleValue) =>
|
||||
{
|
||||
updateFilterValue(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "end",
|
||||
height: 48,
|
||||
}}
|
||||
>
|
||||
<QDynamicSelect
|
||||
tableName={tableName}
|
||||
fieldName={field.name}
|
||||
fieldLabel="Value"
|
||||
initialValue={selectedPossibleValue?.id}
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={handleChange}
|
||||
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////
|
||||
// possible value set operators //
|
||||
//////////////////////////////////
|
||||
export const buildQGridPvsOperators = (tableName: string, field: QFieldMetaData): GridFilterOperator[] =>
|
||||
{
|
||||
return ([
|
||||
{
|
||||
label: "is",
|
||||
value: "is",
|
||||
getApplyFilterFn: () => null,
|
||||
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceSingle(tableName, field, props)
|
||||
},
|
||||
{
|
||||
label: "is not",
|
||||
value: "isNot",
|
||||
getApplyFilterFn: () => null,
|
||||
InputComponent: (props: GridFilterInputValueProps<GridApiCommunity>) => InputPossibleValueSourceSingle(tableName, field, props)
|
||||
},
|
||||
{
|
||||
label: "is empty",
|
||||
value: "isEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
},
|
||||
{
|
||||
label: "is not empty",
|
||||
value: "isNotEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
}
|
||||
]);
|
||||
};
|
@ -21,7 +21,6 @@
|
||||
|
||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
@ -39,14 +38,16 @@ import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import {
|
||||
DataGridPro, getGridDateOperators, getGridNumericOperators, getGridStringOperators,
|
||||
DataGridPro,
|
||||
getGridDateOperators,
|
||||
getGridNumericOperators,
|
||||
GridCallbackDetails,
|
||||
GridColDef,
|
||||
GridColumnOrderChangeParams,
|
||||
GridColumnVisibilityModel,
|
||||
GridExportMenuItemProps,
|
||||
GridFilterItem,
|
||||
GridFilterModel,
|
||||
GridLinkOperator,
|
||||
GridRowId,
|
||||
GridRowParams,
|
||||
GridRowsProp,
|
||||
@ -70,6 +71,7 @@ import Navbar from "qqq/components/Navbar";
|
||||
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
|
||||
import MDAlert from "qqq/components/Temporary/MDAlert";
|
||||
import MDBox from "qqq/components/Temporary/MDBox";
|
||||
import {buildQGridPvsOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/entity-list/QGridFilterOperators";
|
||||
import ProcessRun from "qqq/pages/process-run";
|
||||
import QClient from "qqq/utils/QClient";
|
||||
import QFilterUtils from "qqq/utils/QFilterUtils";
|
||||
@ -92,11 +94,13 @@ EntityList.defaultProps = {
|
||||
launchProcess: null
|
||||
};
|
||||
|
||||
const qController = QClient.getInstance();
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the default filter to use on the page - either from query string, or
|
||||
** local storage, or a default (empty).
|
||||
*******************************************************************************/
|
||||
function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearchParams, filterLocalStorageKey: string): GridFilterModel
|
||||
async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearchParams, filterLocalStorageKey: string): Promise<GridFilterModel>
|
||||
{
|
||||
if (tableMetaData.fields !== undefined)
|
||||
{
|
||||
@ -111,16 +115,39 @@ function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearch
|
||||
//////////////////////////////////////////////////////////////////
|
||||
const defaultFilter = {items: []} as GridFilterModel;
|
||||
let id = 1;
|
||||
qQueryFilter.criteria.forEach((criteria) =>
|
||||
|
||||
for(let i = 0; i < qQueryFilter.criteria.length; i++)
|
||||
{
|
||||
const fieldType = tableMetaData.fields.get(criteria.fieldName).type;
|
||||
const criteria = qQueryFilter.criteria[i];
|
||||
const field = tableMetaData.fields.get(criteria.fieldName);
|
||||
let values = criteria.values;
|
||||
if(field.possibleValueSourceName)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// possible-values in query-string are expected to only be their id values. //
|
||||
// e.g., ...values=[1]... //
|
||||
// but we need them to be possibleValue objects (w/ id & label) so the label //
|
||||
// can be shown in the filter dropdown. So, make backend call to look them up. //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if(values && values.length > 0)
|
||||
{
|
||||
values = await qController.possibleValues(tableMetaData.name, field.name, "", values);
|
||||
}
|
||||
}
|
||||
|
||||
defaultFilter.items.push({
|
||||
columnField: criteria.fieldName,
|
||||
operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, fieldType, criteria.values),
|
||||
value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, fieldType),
|
||||
operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values),
|
||||
value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field),
|
||||
id: id++, // not sure what this id is!!
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
defaultFilter.linkOperator = GridLinkOperator.And;
|
||||
if(qQueryFilter.booleanOperator === "OR")
|
||||
{
|
||||
defaultFilter.linkOperator = GridLinkOperator.Or;
|
||||
}
|
||||
|
||||
return (defaultFilter);
|
||||
}
|
||||
@ -145,7 +172,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
const tableName = table.name;
|
||||
const [searchParams] = useSearchParams();
|
||||
const qController = QClient.getInstance();
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@ -270,8 +296,11 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
setActiveModalProcess(null);
|
||||
}, [location]);
|
||||
|
||||
const buildQFilter = () =>
|
||||
const buildQFilter = (filterModel: GridFilterModel) =>
|
||||
{
|
||||
console.log("Building q filter with model:");
|
||||
console.log(filterModel);
|
||||
|
||||
const qFilter = new QQueryFilter();
|
||||
if (columnSortModel)
|
||||
{
|
||||
@ -280,6 +309,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
|
||||
});
|
||||
}
|
||||
|
||||
if (filterModel)
|
||||
{
|
||||
filterModel.items.forEach((item) =>
|
||||
@ -288,6 +318,15 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
||||
});
|
||||
|
||||
qFilter.booleanOperator = "AND";
|
||||
if(filterModel.linkOperator == "or")
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// by default qFilter uses AND - so only if we see linkOperator=or do we need to set it //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
qFilter.booleanOperator = "OR";
|
||||
}
|
||||
}
|
||||
|
||||
return qFilter;
|
||||
@ -307,10 +346,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
// because we need to know field types to translate qqq filter to material filter //
|
||||
// return here ane wait for the next 'turn' to allow doing the actual query //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let localFilterModel = filterModel;
|
||||
if (!defaultFilterLoaded)
|
||||
{
|
||||
setDefaultFilterLoaded(true);
|
||||
setFilterModel(getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey));
|
||||
localFilterModel = await getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey)
|
||||
setFilterModel(localFilterModel);
|
||||
return;
|
||||
}
|
||||
setTableMetaData(tableMetaData);
|
||||
@ -325,7 +366,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
}
|
||||
setPinnedColumns({left: ["__check__", tableMetaData.primaryKeyField]});
|
||||
|
||||
const qFilter = buildQFilter();
|
||||
const qFilter = buildQFilter(localFilterModel);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// assign a new query id to the query being issued here. then run both the count & query async //
|
||||
@ -394,58 +435,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
delete countResults[latestQueryId];
|
||||
}, [receivedCountTimestamp]);
|
||||
|
||||
const betweenOperator =
|
||||
{
|
||||
label: "Between",
|
||||
value: "between",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem) =>
|
||||
{
|
||||
if (!Array.isArray(filterItem.value) || filterItem.value.length !== 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (filterItem.value[0] == null || filterItem.value[1] == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return ({value}) =>
|
||||
{
|
||||
return (value !== null && filterItem.value[0] <= value && value <= filterItem.value[1]);
|
||||
};
|
||||
},
|
||||
// InputComponent: InputNumberInterval,
|
||||
};
|
||||
|
||||
const booleanTrueOperator: GridFilterOperator = {
|
||||
label: "is yes",
|
||||
value: "isTrue",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanFalseOperator: GridFilterOperator = {
|
||||
label: "is no",
|
||||
value: "isFalse",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanEmptyOperator: GridFilterOperator = {
|
||||
label: "is empty",
|
||||
value: "isEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const booleanNotEmptyOperator: GridFilterOperator = {
|
||||
label: "is not empty",
|
||||
value: "isNotEmpty",
|
||||
getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => null
|
||||
};
|
||||
|
||||
const getCustomGridBooleanOperators = (): GridFilterOperator[] =>
|
||||
{
|
||||
return [booleanTrueOperator, booleanFalseOperator, booleanEmptyOperator, booleanNotEmptyOperator];
|
||||
};
|
||||
|
||||
///////////////////////////
|
||||
// display query results //
|
||||
@ -503,11 +492,11 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
let columnType = "string";
|
||||
let columnWidth = 200;
|
||||
let filterOperators: GridFilterOperator<any>[] = getGridStringOperators();
|
||||
let filterOperators: GridFilterOperator<any>[] = QGridStringOperators;
|
||||
|
||||
if (field.possibleValueSourceName)
|
||||
{
|
||||
filterOperators = getGridNumericOperators();
|
||||
filterOperators = buildQGridPvsOperators(tableName, field);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -523,8 +512,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
columnWidth = 75;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
filterOperators = getGridNumericOperators();
|
||||
filterOperators = QGridNumericOperators;
|
||||
break;
|
||||
case QFieldType.DATE:
|
||||
columnType = "date";
|
||||
@ -539,7 +527,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
case QFieldType.BOOLEAN:
|
||||
columnType = "string"; // using boolean gives an odd 'no' for nulls.
|
||||
columnWidth = 75;
|
||||
filterOperators = getCustomGridBooleanOperators();
|
||||
filterOperators = QGridBooleanOperators;
|
||||
break;
|
||||
default:
|
||||
// noop - leave as string
|
||||
@ -549,35 +537,22 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
if (field.hasAdornment(AdornmentType.SIZE))
|
||||
{
|
||||
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
|
||||
const width = sizeAdornment.getValue("width");
|
||||
switch (width)
|
||||
const width: string = sizeAdornment.getValue("width");
|
||||
const widths: Map<string, number> = new Map<string, number>([
|
||||
["small", 100],
|
||||
["medium", 200],
|
||||
["large", 400],
|
||||
["xlarge", 600]
|
||||
]);
|
||||
if(widths.has(width))
|
||||
{
|
||||
case "small":
|
||||
{
|
||||
columnWidth = 100;
|
||||
break;
|
||||
columnWidth = widths.get(width);
|
||||
}
|
||||
case "medium":
|
||||
{
|
||||
columnWidth = 200;
|
||||
break;
|
||||
}
|
||||
case "large":
|
||||
{
|
||||
columnWidth = 400;
|
||||
break;
|
||||
}
|
||||
case "xlarge":
|
||||
{
|
||||
columnWidth = 600;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
else
|
||||
{
|
||||
console.log("Unrecognized size.width adornment value: " + width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const column = {
|
||||
field: field.name,
|
||||
@ -814,7 +789,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
const d = new Date();
|
||||
const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
|
||||
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
||||
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter()))}&fields=${visibleFields.join(",")}`;
|
||||
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(filterModel)))}&fields=${visibleFields.join(",")}`;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// open a window (tab) with a little page that says the file is being generated. //
|
||||
@ -864,7 +839,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter())}`;
|
||||
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(filterModel))}`;
|
||||
}
|
||||
|
||||
if (selectedIds.length > 0)
|
||||
@ -879,7 +854,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
return (buildQFilter());
|
||||
return (buildQFilter(filterModel));
|
||||
}
|
||||
|
||||
if (selectedIds.length > 0)
|
||||
|
@ -126,17 +126,41 @@
|
||||
}
|
||||
|
||||
/* let long field names in filter dropdown wrap instead of get cut off */
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormColumnInput .MuiNativeSelect-select.MuiNativeSelect-standard
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormColumnInput .MuiNativeSelect-select.MuiNativeSelect-standard,
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput .MuiNativeSelect-select.MuiNativeSelect-standard
|
||||
{
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm
|
||||
{
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* make filter dropdowns a bit wider, less likely to need to wrap. */
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormColumnInput
|
||||
{
|
||||
width: 200px;
|
||||
}
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput
|
||||
{
|
||||
width: 300px;
|
||||
}
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormOperatorInput
|
||||
{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/* Make the drop-down icon for autocompletes match the ones on the native dropdowns. */
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput .MuiAutocomplete-root .MuiAutocomplete-endAdornment
|
||||
{
|
||||
padding-top: 4px;
|
||||
}
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput .MuiAutocomplete-root .MuiAutocomplete-endAdornment svg
|
||||
{
|
||||
height: 0.625em;
|
||||
}
|
||||
|
||||
/* google drive picker - make it be above our modal */
|
||||
.picker,
|
||||
.picker.picker-dialog-bg,
|
||||
@ -144,3 +168,12 @@
|
||||
{
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
/* clears the ‘X’ from Internet Explorer */
|
||||
input[type=search]::-ms-clear { display: none; width : 0; height: 0; }
|
||||
input[type=search]::-ms-reveal { display: none; width : 0; height: 0; }
|
||||
/* clears the ‘X’ from Chrome */
|
||||
input[type="search"]::-webkit-search-decoration,
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-results-button,
|
||||
input[type="search"]::-webkit-search-results-decoration { display: none; }
|
@ -19,8 +19,11 @@
|
||||
* 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 {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import QValueUtils from "qqq/utils/QValueUtils";
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class for working with QQQ Filters
|
||||
@ -37,10 +40,16 @@ class QFilterUtils
|
||||
{
|
||||
case "contains":
|
||||
return QCriteriaOperator.CONTAINS;
|
||||
case "notContains":
|
||||
return QCriteriaOperator.NOT_CONTAINS;
|
||||
case "startsWith":
|
||||
return QCriteriaOperator.STARTS_WITH;
|
||||
case "notStartsWith":
|
||||
return QCriteriaOperator.NOT_STARTS_WITH;
|
||||
case "endsWith":
|
||||
return QCriteriaOperator.ENDS_WITH;
|
||||
case "notEndsWith":
|
||||
return QCriteriaOperator.NOT_ENDS_WITH;
|
||||
case "is":
|
||||
case "equals":
|
||||
case "=":
|
||||
@ -68,8 +77,12 @@ class QFilterUtils
|
||||
return QCriteriaOperator.IS_NOT_BLANK;
|
||||
case "isAnyOf":
|
||||
return QCriteriaOperator.IN;
|
||||
case "isNone": // todo - verify - not seen in UI
|
||||
case "isNone":
|
||||
return QCriteriaOperator.NOT_IN;
|
||||
case "between":
|
||||
return QCriteriaOperator.BETWEEN;
|
||||
case "notBetween":
|
||||
return QCriteriaOperator.NOT_BETWEEN;
|
||||
default:
|
||||
return QCriteriaOperator.EQUALS;
|
||||
}
|
||||
@ -78,11 +91,18 @@ class QFilterUtils
|
||||
/*******************************************************************************
|
||||
** Convert a qqq criteria operator to one expected by the grid.
|
||||
*******************************************************************************/
|
||||
public static qqqCriteriaOperatorToGrid = (operator: QCriteriaOperator, fieldType: QFieldType = QFieldType.STRING, criteriaValues: any[]): string =>
|
||||
public static qqqCriteriaOperatorToGrid = (operator: QCriteriaOperator, field: QFieldMetaData, criteriaValues: any[]): string =>
|
||||
{
|
||||
const fieldType = field.type;
|
||||
switch (operator)
|
||||
{
|
||||
case QCriteriaOperator.EQUALS:
|
||||
|
||||
if(field.possibleValueSourceName)
|
||||
{
|
||||
return ("is");
|
||||
}
|
||||
|
||||
switch (fieldType)
|
||||
{
|
||||
case QFieldType.INTEGER:
|
||||
@ -95,13 +115,13 @@ class QFilterUtils
|
||||
case QFieldType.BOOLEAN:
|
||||
if (criteriaValues && criteriaValues[0] === true)
|
||||
{
|
||||
return "isTrue";
|
||||
return ("isTrue");
|
||||
}
|
||||
else if (criteriaValues && criteriaValues[0] === false)
|
||||
{
|
||||
return "isFalse";
|
||||
return ("isFalse");
|
||||
}
|
||||
return "is";
|
||||
return ("is");
|
||||
case QFieldType.STRING:
|
||||
case QFieldType.TEXT:
|
||||
case QFieldType.HTML:
|
||||
@ -111,6 +131,12 @@ class QFilterUtils
|
||||
return ("is");
|
||||
}
|
||||
case QCriteriaOperator.NOT_EQUALS:
|
||||
|
||||
if(field.possibleValueSourceName)
|
||||
{
|
||||
return ("isNot");
|
||||
}
|
||||
|
||||
switch (fieldType)
|
||||
{
|
||||
case QFieldType.INTEGER:
|
||||
@ -131,7 +157,7 @@ class QFilterUtils
|
||||
case QCriteriaOperator.IN:
|
||||
return ("isAnyOf");
|
||||
case QCriteriaOperator.NOT_IN:
|
||||
return ("isNone"); // todo verify - not seen in UI
|
||||
return ("isNone");
|
||||
case QCriteriaOperator.STARTS_WITH:
|
||||
return ("startsWith");
|
||||
case QCriteriaOperator.ENDS_WITH:
|
||||
@ -139,11 +165,11 @@ class QFilterUtils
|
||||
case QCriteriaOperator.CONTAINS:
|
||||
return ("contains");
|
||||
case QCriteriaOperator.NOT_STARTS_WITH:
|
||||
return (""); // todo - not supported in grid?
|
||||
return ("notStartsWith");
|
||||
case QCriteriaOperator.NOT_ENDS_WITH:
|
||||
return (""); // todo - not supported in grid?
|
||||
return ("notEndsWith");
|
||||
case QCriteriaOperator.NOT_CONTAINS:
|
||||
return (""); // todo - not supported in grid?
|
||||
return ("notContains");
|
||||
case QCriteriaOperator.LESS_THAN:
|
||||
switch (fieldType)
|
||||
{
|
||||
@ -189,9 +215,9 @@ class QFilterUtils
|
||||
case QCriteriaOperator.IS_NOT_BLANK:
|
||||
return ("isNotEmpty");
|
||||
case QCriteriaOperator.BETWEEN:
|
||||
return (""); // todo - not supported in grid?
|
||||
return ("between");
|
||||
case QCriteriaOperator.NOT_BETWEEN:
|
||||
return (""); // todo - not supported in grid?
|
||||
return ("notBetween");
|
||||
default:
|
||||
console.warn(`Unhandled criteria operator: ${operator}`);
|
||||
return ("=");
|
||||
@ -220,24 +246,65 @@ class QFilterUtils
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN)
|
||||
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
|
||||
{
|
||||
return (value);
|
||||
if(value == null && (operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we send back null, we get a 500 - bad look every time you try to set up a BETWEEN filter //
|
||||
// but array of 2 nulls? comes up sunshine. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return ([null, null]);
|
||||
}
|
||||
return (QFilterUtils.extractIdsFromPossibleValueList(value));
|
||||
}
|
||||
|
||||
return ([value]);
|
||||
return (QFilterUtils.extractIdsFromPossibleValueList([value]));
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Helper method - take a list of values, which may be possible values, and
|
||||
** either return the original list, or a new list that is just the ids of the
|
||||
** possible values (if it was a list of possible values)
|
||||
*******************************************************************************/
|
||||
public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], fieldType: QFieldType): any | any[] =>
|
||||
private static extractIdsFromPossibleValueList = (param: any[]): number[] | string[] =>
|
||||
{
|
||||
if(param === null || param === undefined)
|
||||
{
|
||||
return (param);
|
||||
}
|
||||
|
||||
let rs = [];
|
||||
for(let i = 0; i < param.length; i++)
|
||||
{
|
||||
console.log(param[i]);
|
||||
if(param[i] && param[i].id && param[i].label)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
// if the param looks like a possible value, return its id //
|
||||
/////////////////////////////////////////////////////////////
|
||||
rs.push(param[i].id);
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.push(param[i]);
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Convert a filter field's value from the style that qqq uses, to the style that
|
||||
** the grid uses.
|
||||
*******************************************************************************/
|
||||
public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], field: QFieldMetaData): any | any[] =>
|
||||
{
|
||||
const fieldType = field.type;
|
||||
if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK)
|
||||
{
|
||||
return (null); // todo - verify
|
||||
return (null);
|
||||
}
|
||||
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN)
|
||||
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
|
||||
{
|
||||
return (values);
|
||||
}
|
||||
@ -249,21 +316,7 @@ class QFilterUtils
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (fieldType === QFieldType.DATE_TIME)
|
||||
{
|
||||
const inputValue = values[0];
|
||||
if(inputValue.match(/^\d{4}-\d{2}-\d{2}$/))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// if we just passed in a date (w/o time), attach T00:00 to it. //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
values[0] = inputValue + "T00:00";
|
||||
}
|
||||
else if(inputValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}.*/))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if we passed in something too long (e.g., w/ seconds and fractions), trim it. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
values[0] = inputValue.substring(0, 16);
|
||||
}
|
||||
values[0] = QValueUtils.formatDateTimeValueForForm(values[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,6 +218,40 @@ class QValueUtils
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Take a date-time value, and format it the way the ui's date-times want it
|
||||
** to be.
|
||||
*******************************************************************************/
|
||||
public static formatDateTimeValueForForm(value: string): string
|
||||
{
|
||||
if(value === null || value === undefined)
|
||||
{
|
||||
return (value);
|
||||
}
|
||||
|
||||
if(value.match(/^\d{4}-\d{2}-\d{2}$/))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// if we just passed in a date (w/o time), attach T00:00 to it. //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
return(value + "T00:00");
|
||||
}
|
||||
else if(value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}.*/))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if we passed in something too long (e.g., w/ seconds and fractions), trim it. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
return(value.substring(0, 16));
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// by default, return the input value //
|
||||
////////////////////////////////////////
|
||||
return (value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default QValueUtils;
|
||||
|
Reference in New Issue
Block a user