Adding possible-value dropdowns to forms and filters

This commit is contained in:
2022-10-11 08:26:43 -05:00
parent 0e6c20d6d6
commit 0e32acce21
11 changed files with 828 additions and 147 deletions

View File

@ -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)

View File

@ -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 (

View File

@ -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>}

View 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;

View File

@ -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) => (