/*
* 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 .
*/
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {Checkbox, Chip, CircularProgress, FilterOptionsState, Icon} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
import {ErrorMessage, useFormikContext} from "formik";
import React, {useEffect, useState} from "react";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography";
import Client from "qqq/utils/qqq/Client";
interface Props
{
tableName?: string;
processName?: string;
fieldName: string;
overrideId?: string;
fieldLabel: string;
inForm: boolean;
initialValue?: any;
initialDisplayValue?: string;
initialValues?: QPossibleValue[];
onChange?: any;
isEditable?: boolean;
isMultiple?: boolean;
bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any;
otherValues?: Map;
variant: "standard" | "outlined";
initiallyOpen: boolean;
}
DynamicSelect.defaultProps = {
tableName: null,
processName: null,
inForm: true,
initialValue: null,
initialDisplayValue: null,
initialValues: undefined,
onChange: null,
isEditable: true,
isMultiple: false,
bulkEditMode: false,
otherValues: new Map(),
variant: "outlined",
initiallyOpen: false,
bulkEditSwitchChangeHandler: () =>
{
},
};
const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
{
const [open, setOpen] = useState(initiallyOpen);
const [options, setOptions] = useState([]);
const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true);
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
const {inputBorderColor} = colors;
////////////////////////////////////////////////////////////////////////////////////////////////
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
// else non-multiple, assume we took in an initialValue (id) and initialDisplayValue (label), //
// and build a little object that looks like a possibleValue out of those //
////////////////////////////////////////////////////////////////////////////////////////////////
let [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined)
: useState(initialValue && initialDisplayValue ? [{id: initialValue, label: initialDisplayValue}] : null);
if (isMultiple && defaultValue === null)
{
defaultValue = [];
}
// const loading = open && options.length === 0;
const [loading, setLoading] = useState(false);
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
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);
/*
if(!initiallyOpen)
{
console.log("returning because not initially open?");
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, processName, fieldName, searchTerm ?? "", null, otherValues);
if(tableMetaData == null && tableName)
{
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
}
setLoading(false);
// console.log("Results:")
// console.log(`${results}`);
if (active)
{
setOptions([ ...results ]);
}
})();
return () =>
{
active = false;
};
}, [ searchTerm ]);
// todo - finish... call it in onOpen?
const reloadIfOtherValuesAreChanged = () =>
{
if(JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
{
(async () =>
{
setLoading(true);
setOptions([]);
console.log("Refreshing possible values...");
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName, searchTerm ?? "", null, otherValues);
setLoading(false);
setOptions([ ...results ]);
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
})();
}
}
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)
{
if(isMultiple)
{
onChange(value);
}
else
{
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);
}
// @ts-ignore
const renderOption = (props: Object, option: any, {selected}) =>
{
let content = (<>{option.label}>);
try
{
const field = tableMetaData?.fields.get(fieldName)
if(field)
{
const adornment = field.getAdornment(AdornmentType.CHIP);
if(adornment)
{
const color = adornment.getValue("color." + option.id) ?? "default"
const iconName = adornment.getValue("icon." + option.id) ?? null;
const iconElement = iconName ? {iconName} : null;
content = ();
}
}
}
catch(e)
{ }
if(isMultiple)
{
content = (
<>
{content}
>
);
}
///////////////////////////////////////////////////////////////////////////////////////////////
// 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 (
{content}
);
}
const bulkEditSwitchChanged = () =>
{
const newSwitchValue = !switchChecked;
setSwitchChecked(newSwitchValue);
setIsDisabled(!newSwitchValue);
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
};
////////////////////////////////////////////
// for outlined style, adjust some styles //
////////////////////////////////////////////
let autocompleteSX = {};
if (variant == "outlined")
{
autocompleteSX = {
"& .MuiOutlinedInput-root": {
borderRadius: "0.75rem",
},
"& .MuiInputBase-root": {
padding: "0.5rem",
background: isDisabled ? "#f0f2f5!important" : "initial",
},
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
padding: "0",
fontSize: "1rem"
},
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
borderColor: inputBorderColor
}
}
}
const autocomplete = (
{
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) => value !== null && value !== undefined && option.id === value.id}
getOptionLabel={(option) =>
{
if(option === null || option === undefined)
{
return ("");
}
// @ts-ignore
if(option && option.length)
{
// @ts-ignore
option = option[0];
}
// @ts-ignore
return 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}
disabled={isDisabled}
multiple={isMultiple}
disableCloseOnSelect={isMultiple}
limitTags={5}
slotProps={{popper: {className: "DynamicSelectPopper"}}}
renderInput={(params) => (
{loading ? : null}
{params.InputProps.endAdornment}
),
}}
/>
)}
/>
{
inForm &&
{!isDisabled && {msg}} />
}
}
);
if (bulkEditMode)
{
return (
{autocomplete}
);
}
else
{
return (
{autocomplete}
);
}
}
export default DynamicSelect;