/* * 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;