mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10: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/interaction": "5.10.0",
|
||||||
"@fullcalendar/react": "5.10.0",
|
"@fullcalendar/react": "5.10.0",
|
||||||
"@fullcalendar/timegrid": "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/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.4.1",
|
"@mui/material": "5.4.1",
|
||||||
"@mui/styled-engine": "5.4.1",
|
"@mui/styled-engine": "5.4.1",
|
||||||
"@mui/styles": "5.10.7",
|
"@mui/styles": "5.10.7",
|
||||||
"@mui/x-data-grid": "5.13.0",
|
"@mui/x-data-grid": "5.17.6",
|
||||||
"@mui/x-data-grid-pro": "5.13.0",
|
"@mui/x-data-grid-pro": "5.17.6",
|
||||||
"@mui/x-license-pro": "5.12.3",
|
"@mui/x-license-pro": "5.12.3",
|
||||||
"@react-jvectormap/core": "1.0.1",
|
"@react-jvectormap/core": "1.0.1",
|
||||||
"@react-jvectormap/unitedstates": "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 {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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
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 MDTypography from "qqq/components/Temporary/MDTypography";
|
||||||
import QClient from "qqq/utils/QClient";
|
import QClient from "qqq/utils/QClient";
|
||||||
import QTableUtils from "qqq/utils/QTableUtils";
|
import QTableUtils from "qqq/utils/QTableUtils";
|
||||||
|
import QValueUtils from "qqq/utils/QValueUtils";
|
||||||
|
|
||||||
interface Props
|
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 //
|
// if doing an edit, fetch the record and pre-populate the form values from it //
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let record: QRecord = null;
|
||||||
if (id !== null)
|
if (id !== null)
|
||||||
{
|
{
|
||||||
const record = await qController.get(tableName, id);
|
record = await qController.get(tableName, id);
|
||||||
setRecord(record);
|
setRecord(record);
|
||||||
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
|
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||||
setPageHeader(`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) =>
|
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
||||||
{
|
{
|
||||||
initialValues[key] = record.values.get(key);
|
initialValues[key] = record.values.get(key);
|
||||||
|
if(fieldMetaData.type == QFieldType.DATE_TIME)
|
||||||
|
{
|
||||||
|
initialValues[key] = QValueUtils.formatDateTimeValueForForm(record.values.get(key));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setFormValues(formValues);
|
setFormValues(formValues);
|
||||||
@ -173,6 +180,24 @@ function EntityForm({table, id}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
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)
|
if (sectionDynamicFormFields.length === 0)
|
||||||
@ -245,7 +270,9 @@ function EntityForm({table, id}: Props): JSX.Element
|
|||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
{
|
{
|
||||||
setAlertContent(error.response.data.error);
|
console.log("Caught:");
|
||||||
|
console.log(error);
|
||||||
|
setAlertContent(error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -259,7 +286,7 @@ function EntityForm({table, id}: Props): JSX.Element
|
|||||||
})
|
})
|
||||||
.catch((error) =>
|
.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">
|
<Form id={formId} autoComplete="off">
|
||||||
|
|
||||||
<MDBox pb={3} pt={0}>
|
<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 display="flex" p={3} pb={1}>
|
||||||
<MDBox mr={1.5}>
|
<MDBox mr={1.5}>
|
||||||
<Avatar sx={{bgcolor: colors.info.main}}>
|
<Avatar sx={{bgcolor: colors.info.main}}>
|
||||||
@ -313,7 +340,7 @@ function EntityForm({table, id}: Props): JSX.Element
|
|||||||
</MDBox>
|
</MDBox>
|
||||||
{
|
{
|
||||||
t1sectionName && formFields ? (
|
t1sectionName && formFields ? (
|
||||||
<MDBox pb={3} px={3}>
|
<MDBox pb={1} px={3}>
|
||||||
<MDBox p={3} width="100%">
|
<MDBox p={3} width="100%">
|
||||||
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
|
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
|
||||||
</MDBox>
|
</MDBox>
|
||||||
@ -324,11 +351,11 @@ function EntityForm({table, id}: Props): JSX.Element
|
|||||||
</MDBox>
|
</MDBox>
|
||||||
{formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
|
{formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
|
||||||
<MDBox key={`edit-card-${section.name}`} pb={3}>
|
<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}>
|
<MDTypography variant="h5" p={3} pb={1}>
|
||||||
{section.label}
|
{section.label}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
<MDBox pb={3} px={3}>
|
<MDBox pb={1} px={3}>
|
||||||
<MDBox p={3} width="100%">
|
<MDBox p={3} width="100%">
|
||||||
{
|
{
|
||||||
getFormSection(values, touched, formFields.get(section.name), errors)
|
getFormSection(values, touched, formFields.get(section.name), errors)
|
||||||
|
@ -19,13 +19,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {colors} from "@mui/material";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import {useFormikContext} from "formik";
|
import {useFormikContext} from "formik";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import QDynamicFormField from "qqq/components/QDynamicFormField";
|
import QDynamicFormField from "qqq/components/QDynamicFormField";
|
||||||
|
import QDynamicSelect from "qqq/components/QDynamicSelect/QDynamicSelect";
|
||||||
import MDBox from "qqq/components/Temporary/MDBox";
|
import MDBox from "qqq/components/Temporary/MDBox";
|
||||||
import MDTypography from "qqq/components/Temporary/MDTypography";
|
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? inputProps={{ autoComplete: "" }}
|
||||||
// todo? placeholder={password.placeholder}
|
// todo? placeholder={password.placeholder}
|
||||||
return (
|
return (
|
||||||
|
@ -87,7 +87,15 @@ function QDynamicFormField({
|
|||||||
(type == "checkbox" ?
|
(type == "checkbox" ?
|
||||||
<QBooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} /> :
|
<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}>
|
<MDBox mt={0.75}>
|
||||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} /></div>}
|
{!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 (
|
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"}}>
|
<MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
||||||
{
|
{
|
||||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
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 {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
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 {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
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 MenuItem from "@mui/material/MenuItem";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
import {
|
import {
|
||||||
DataGridPro, getGridDateOperators, getGridNumericOperators, getGridStringOperators,
|
DataGridPro,
|
||||||
|
getGridDateOperators,
|
||||||
|
getGridNumericOperators,
|
||||||
GridCallbackDetails,
|
GridCallbackDetails,
|
||||||
GridColDef,
|
GridColDef,
|
||||||
GridColumnOrderChangeParams,
|
GridColumnOrderChangeParams,
|
||||||
GridColumnVisibilityModel,
|
GridColumnVisibilityModel,
|
||||||
GridExportMenuItemProps,
|
GridExportMenuItemProps,
|
||||||
GridFilterItem,
|
|
||||||
GridFilterModel,
|
GridFilterModel,
|
||||||
|
GridLinkOperator,
|
||||||
GridRowId,
|
GridRowId,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridRowsProp,
|
GridRowsProp,
|
||||||
@ -70,6 +71,7 @@ import Navbar from "qqq/components/Navbar";
|
|||||||
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
|
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
|
||||||
import MDAlert from "qqq/components/Temporary/MDAlert";
|
import MDAlert from "qqq/components/Temporary/MDAlert";
|
||||||
import MDBox from "qqq/components/Temporary/MDBox";
|
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 ProcessRun from "qqq/pages/process-run";
|
||||||
import QClient from "qqq/utils/QClient";
|
import QClient from "qqq/utils/QClient";
|
||||||
import QFilterUtils from "qqq/utils/QFilterUtils";
|
import QFilterUtils from "qqq/utils/QFilterUtils";
|
||||||
@ -92,11 +94,13 @@ EntityList.defaultProps = {
|
|||||||
launchProcess: null
|
launchProcess: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const qController = QClient.getInstance();
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Get the default filter to use on the page - either from query string, or
|
** Get the default filter to use on the page - either from query string, or
|
||||||
** local storage, or a default (empty).
|
** 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)
|
if (tableMetaData.fields !== undefined)
|
||||||
{
|
{
|
||||||
@ -111,16 +115,39 @@ function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearch
|
|||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
const defaultFilter = {items: []} as GridFilterModel;
|
const defaultFilter = {items: []} as GridFilterModel;
|
||||||
let id = 1;
|
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({
|
defaultFilter.items.push({
|
||||||
columnField: criteria.fieldName,
|
columnField: criteria.fieldName,
|
||||||
operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, fieldType, criteria.values),
|
operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values),
|
||||||
value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, fieldType),
|
value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field),
|
||||||
id: id++, // not sure what this id is!!
|
id: id++, // not sure what this id is!!
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
defaultFilter.linkOperator = GridLinkOperator.And;
|
||||||
|
if(qQueryFilter.booleanOperator === "OR")
|
||||||
|
{
|
||||||
|
defaultFilter.linkOperator = GridLinkOperator.Or;
|
||||||
|
}
|
||||||
|
|
||||||
return (defaultFilter);
|
return (defaultFilter);
|
||||||
}
|
}
|
||||||
@ -145,7 +172,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const qController = QClient.getInstance();
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -270,8 +296,11 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
setActiveModalProcess(null);
|
setActiveModalProcess(null);
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
const buildQFilter = () =>
|
const buildQFilter = (filterModel: GridFilterModel) =>
|
||||||
{
|
{
|
||||||
|
console.log("Building q filter with model:");
|
||||||
|
console.log(filterModel);
|
||||||
|
|
||||||
const qFilter = new QQueryFilter();
|
const qFilter = new QQueryFilter();
|
||||||
if (columnSortModel)
|
if (columnSortModel)
|
||||||
{
|
{
|
||||||
@ -280,6 +309,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
|
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterModel)
|
if (filterModel)
|
||||||
{
|
{
|
||||||
filterModel.items.forEach((item) =>
|
filterModel.items.forEach((item) =>
|
||||||
@ -288,6 +318,15 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
||||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
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;
|
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 //
|
// 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 //
|
// return here ane wait for the next 'turn' to allow doing the actual query //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let localFilterModel = filterModel;
|
||||||
if (!defaultFilterLoaded)
|
if (!defaultFilterLoaded)
|
||||||
{
|
{
|
||||||
setDefaultFilterLoaded(true);
|
setDefaultFilterLoaded(true);
|
||||||
setFilterModel(getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey));
|
localFilterModel = await getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey)
|
||||||
|
setFilterModel(localFilterModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
@ -325,7 +366,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
setPinnedColumns({left: ["__check__", tableMetaData.primaryKeyField]});
|
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 //
|
// 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];
|
delete countResults[latestQueryId];
|
||||||
}, [receivedCountTimestamp]);
|
}, [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 //
|
// display query results //
|
||||||
@ -503,11 +492,11 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
let columnType = "string";
|
let columnType = "string";
|
||||||
let columnWidth = 200;
|
let columnWidth = 200;
|
||||||
let filterOperators: GridFilterOperator<any>[] = getGridStringOperators();
|
let filterOperators: GridFilterOperator<any>[] = QGridStringOperators;
|
||||||
|
|
||||||
if (field.possibleValueSourceName)
|
if (field.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
filterOperators = getGridNumericOperators();
|
filterOperators = buildQGridPvsOperators(tableName, field);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -523,8 +512,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
columnWidth = 75;
|
columnWidth = 75;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
filterOperators = QGridNumericOperators;
|
||||||
filterOperators = getGridNumericOperators();
|
|
||||||
break;
|
break;
|
||||||
case QFieldType.DATE:
|
case QFieldType.DATE:
|
||||||
columnType = "date";
|
columnType = "date";
|
||||||
@ -539,7 +527,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
case QFieldType.BOOLEAN:
|
case QFieldType.BOOLEAN:
|
||||||
columnType = "string"; // using boolean gives an odd 'no' for nulls.
|
columnType = "string"; // using boolean gives an odd 'no' for nulls.
|
||||||
columnWidth = 75;
|
columnWidth = 75;
|
||||||
filterOperators = getCustomGridBooleanOperators();
|
filterOperators = QGridBooleanOperators;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// noop - leave as string
|
// noop - leave as string
|
||||||
@ -549,33 +537,20 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
if (field.hasAdornment(AdornmentType.SIZE))
|
if (field.hasAdornment(AdornmentType.SIZE))
|
||||||
{
|
{
|
||||||
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
|
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
|
||||||
const width = sizeAdornment.getValue("width");
|
const width: string = sizeAdornment.getValue("width");
|
||||||
switch (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 = widths.get(width);
|
||||||
{
|
}
|
||||||
columnWidth = 100;
|
else
|
||||||
break;
|
{
|
||||||
}
|
console.log("Unrecognized size.width adornment value: " + width);
|
||||||
case "medium":
|
|
||||||
{
|
|
||||||
columnWidth = 200;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "large":
|
|
||||||
{
|
|
||||||
columnWidth = 400;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "xlarge":
|
|
||||||
{
|
|
||||||
columnWidth = 600;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
console.log("Unrecognized size.width adornment value: " + width);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,7 +789,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const d = new Date();
|
const d = new Date();
|
||||||
const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
|
const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
|
||||||
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
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. //
|
// 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")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter())}`;
|
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(filterModel))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIds.length > 0)
|
if (selectedIds.length > 0)
|
||||||
@ -879,7 +854,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if (selectFullFilterState === "filter")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
return (buildQFilter());
|
return (buildQFilter(filterModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIds.length > 0)
|
if (selectedIds.length > 0)
|
||||||
|
@ -126,17 +126,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* let long field names in filter dropdown wrap instead of get cut off */
|
/* 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;
|
white-space: normal;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiDataGrid-filterForm
|
.MuiDataGrid-filterForm
|
||||||
{
|
{
|
||||||
align-items: flex-end;
|
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 */
|
/* google drive picker - make it be above our modal */
|
||||||
.picker,
|
.picker,
|
||||||
.picker.picker-dialog-bg,
|
.picker.picker-dialog-bg,
|
||||||
@ -144,3 +168,12 @@
|
|||||||
{
|
{
|
||||||
z-index: 99999;
|
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/>.
|
* 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 {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 {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
|
import QValueUtils from "qqq/utils/QValueUtils";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for working with QQQ Filters
|
** Utility class for working with QQQ Filters
|
||||||
@ -37,10 +40,16 @@ class QFilterUtils
|
|||||||
{
|
{
|
||||||
case "contains":
|
case "contains":
|
||||||
return QCriteriaOperator.CONTAINS;
|
return QCriteriaOperator.CONTAINS;
|
||||||
|
case "notContains":
|
||||||
|
return QCriteriaOperator.NOT_CONTAINS;
|
||||||
case "startsWith":
|
case "startsWith":
|
||||||
return QCriteriaOperator.STARTS_WITH;
|
return QCriteriaOperator.STARTS_WITH;
|
||||||
|
case "notStartsWith":
|
||||||
|
return QCriteriaOperator.NOT_STARTS_WITH;
|
||||||
case "endsWith":
|
case "endsWith":
|
||||||
return QCriteriaOperator.ENDS_WITH;
|
return QCriteriaOperator.ENDS_WITH;
|
||||||
|
case "notEndsWith":
|
||||||
|
return QCriteriaOperator.NOT_ENDS_WITH;
|
||||||
case "is":
|
case "is":
|
||||||
case "equals":
|
case "equals":
|
||||||
case "=":
|
case "=":
|
||||||
@ -68,8 +77,12 @@ class QFilterUtils
|
|||||||
return QCriteriaOperator.IS_NOT_BLANK;
|
return QCriteriaOperator.IS_NOT_BLANK;
|
||||||
case "isAnyOf":
|
case "isAnyOf":
|
||||||
return QCriteriaOperator.IN;
|
return QCriteriaOperator.IN;
|
||||||
case "isNone": // todo - verify - not seen in UI
|
case "isNone":
|
||||||
return QCriteriaOperator.NOT_IN;
|
return QCriteriaOperator.NOT_IN;
|
||||||
|
case "between":
|
||||||
|
return QCriteriaOperator.BETWEEN;
|
||||||
|
case "notBetween":
|
||||||
|
return QCriteriaOperator.NOT_BETWEEN;
|
||||||
default:
|
default:
|
||||||
return QCriteriaOperator.EQUALS;
|
return QCriteriaOperator.EQUALS;
|
||||||
}
|
}
|
||||||
@ -78,11 +91,18 @@ class QFilterUtils
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Convert a qqq criteria operator to one expected by the grid.
|
** 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)
|
switch (operator)
|
||||||
{
|
{
|
||||||
case QCriteriaOperator.EQUALS:
|
case QCriteriaOperator.EQUALS:
|
||||||
|
|
||||||
|
if(field.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
return ("is");
|
||||||
|
}
|
||||||
|
|
||||||
switch (fieldType)
|
switch (fieldType)
|
||||||
{
|
{
|
||||||
case QFieldType.INTEGER:
|
case QFieldType.INTEGER:
|
||||||
@ -95,13 +115,13 @@ class QFilterUtils
|
|||||||
case QFieldType.BOOLEAN:
|
case QFieldType.BOOLEAN:
|
||||||
if (criteriaValues && criteriaValues[0] === true)
|
if (criteriaValues && criteriaValues[0] === true)
|
||||||
{
|
{
|
||||||
return "isTrue";
|
return ("isTrue");
|
||||||
}
|
}
|
||||||
else if (criteriaValues && criteriaValues[0] === false)
|
else if (criteriaValues && criteriaValues[0] === false)
|
||||||
{
|
{
|
||||||
return "isFalse";
|
return ("isFalse");
|
||||||
}
|
}
|
||||||
return "is";
|
return ("is");
|
||||||
case QFieldType.STRING:
|
case QFieldType.STRING:
|
||||||
case QFieldType.TEXT:
|
case QFieldType.TEXT:
|
||||||
case QFieldType.HTML:
|
case QFieldType.HTML:
|
||||||
@ -111,6 +131,12 @@ class QFilterUtils
|
|||||||
return ("is");
|
return ("is");
|
||||||
}
|
}
|
||||||
case QCriteriaOperator.NOT_EQUALS:
|
case QCriteriaOperator.NOT_EQUALS:
|
||||||
|
|
||||||
|
if(field.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
return ("isNot");
|
||||||
|
}
|
||||||
|
|
||||||
switch (fieldType)
|
switch (fieldType)
|
||||||
{
|
{
|
||||||
case QFieldType.INTEGER:
|
case QFieldType.INTEGER:
|
||||||
@ -131,7 +157,7 @@ class QFilterUtils
|
|||||||
case QCriteriaOperator.IN:
|
case QCriteriaOperator.IN:
|
||||||
return ("isAnyOf");
|
return ("isAnyOf");
|
||||||
case QCriteriaOperator.NOT_IN:
|
case QCriteriaOperator.NOT_IN:
|
||||||
return ("isNone"); // todo verify - not seen in UI
|
return ("isNone");
|
||||||
case QCriteriaOperator.STARTS_WITH:
|
case QCriteriaOperator.STARTS_WITH:
|
||||||
return ("startsWith");
|
return ("startsWith");
|
||||||
case QCriteriaOperator.ENDS_WITH:
|
case QCriteriaOperator.ENDS_WITH:
|
||||||
@ -139,11 +165,11 @@ class QFilterUtils
|
|||||||
case QCriteriaOperator.CONTAINS:
|
case QCriteriaOperator.CONTAINS:
|
||||||
return ("contains");
|
return ("contains");
|
||||||
case QCriteriaOperator.NOT_STARTS_WITH:
|
case QCriteriaOperator.NOT_STARTS_WITH:
|
||||||
return (""); // todo - not supported in grid?
|
return ("notStartsWith");
|
||||||
case QCriteriaOperator.NOT_ENDS_WITH:
|
case QCriteriaOperator.NOT_ENDS_WITH:
|
||||||
return (""); // todo - not supported in grid?
|
return ("notEndsWith");
|
||||||
case QCriteriaOperator.NOT_CONTAINS:
|
case QCriteriaOperator.NOT_CONTAINS:
|
||||||
return (""); // todo - not supported in grid?
|
return ("notContains");
|
||||||
case QCriteriaOperator.LESS_THAN:
|
case QCriteriaOperator.LESS_THAN:
|
||||||
switch (fieldType)
|
switch (fieldType)
|
||||||
{
|
{
|
||||||
@ -189,9 +215,9 @@ class QFilterUtils
|
|||||||
case QCriteriaOperator.IS_NOT_BLANK:
|
case QCriteriaOperator.IS_NOT_BLANK:
|
||||||
return ("isNotEmpty");
|
return ("isNotEmpty");
|
||||||
case QCriteriaOperator.BETWEEN:
|
case QCriteriaOperator.BETWEEN:
|
||||||
return (""); // todo - not supported in grid?
|
return ("between");
|
||||||
case QCriteriaOperator.NOT_BETWEEN:
|
case QCriteriaOperator.NOT_BETWEEN:
|
||||||
return (""); // todo - not supported in grid?
|
return ("notBetween");
|
||||||
default:
|
default:
|
||||||
console.warn(`Unhandled criteria operator: ${operator}`);
|
console.warn(`Unhandled criteria operator: ${operator}`);
|
||||||
return ("=");
|
return ("=");
|
||||||
@ -220,24 +246,65 @@ class QFilterUtils
|
|||||||
{
|
{
|
||||||
return (null);
|
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)
|
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);
|
return (values);
|
||||||
}
|
}
|
||||||
@ -249,21 +316,7 @@ class QFilterUtils
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (fieldType === QFieldType.DATE_TIME)
|
if (fieldType === QFieldType.DATE_TIME)
|
||||||
{
|
{
|
||||||
const inputValue = values[0];
|
values[0] = QValueUtils.formatDateTimeValueForForm(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +218,40 @@ class QValueUtils
|
|||||||
</Fragment>
|
</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;
|
export default QValueUtils;
|
||||||
|
Reference in New Issue
Block a user