diff --git a/src/components/index.js b/src/components/index.js index c6fc905c..bcf680af 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -84,6 +84,7 @@ export {TotalRow as MuiTotalRow, NotesRow as MuiNotesRow} from './mui/table/extr export {default as MuiFormikAsyncSelect} from './mui/formik-inputs/mui-formik-async-select' export {default as MuiFormikCheckboxGroup} from './mui/formik-inputs/mui-formik-checkbox-group' export {default as MuiFormikCheckbox} from './mui/formik-inputs/mui-formik-checkbox' +export {default as MuiFormikColorInput} from './mui/formik-inputs/mui-formik-color-input' export {default as MuiFormikDatepicker} from './mui/formik-inputs/mui-formik-datepicker' export {default as MuiFormikDiscountField} from './mui/formik-inputs/mui-formik-discountfield' export {default as MuiFormikDropdownCheckbox} from './mui/formik-inputs/mui-formik-dropdown-checkbox' diff --git a/src/components/mui/formik-inputs/mui-formik-async-select.js b/src/components/mui/formik-inputs/mui-formik-async-select.js index 8f724fb6..13d72181 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -20,25 +20,33 @@ import { } from "@mui/material"; import { useField } from "formik"; import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; +import PropTypes from "prop-types"; +/** + * Async Autocomplete with two modes: + * - Remote (default): fetches options from API on each user input (debounced). + * - Local (localFilter=true): fetches once on mount and filters options client-side. + * Note: localFilter mode assumes stable queryParams (set once on mount). + * If queryParams need to change, remount the component instead. + */ const MuiFormikAsyncAutocomplete = ({ name, queryFunction, - multiple = false, placeholder = "Select...", plainValue = false, hiddenOptions = [], formatOption = (item) => ({ value: item.id.toString(), label: item.name }), formatSelectedValue = null, queryParams = [], - isMulti = false + isMulti = false, + localFilter = false }) => { const [field, meta, helpers] = useField(name); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const value = field.value || (multiple ? [] : null); + const value = field.value || (isMulti ? [] : null); const error = meta.touched && meta.error; const fetchOptions = async (input = "") => { @@ -58,7 +66,7 @@ const MuiFormikAsyncAutocomplete = ({ }; useEffect(() => { - if (searchTerm) { + if (!localFilter && searchTerm) { const delayDebounce = setTimeout(() => { fetchOptions(searchTerm); }, DEBOUNCE_WAIT_250); @@ -72,7 +80,7 @@ const MuiFormikAsyncAutocomplete = ({ }, []); const handleChange = (event, selected) => { - if (!multiple) { + if (!isMulti) { const selectedValue = plainValue ? selected?.value || "" : selected; helpers.setValue(selectedValue); return; @@ -81,10 +89,10 @@ const MuiFormikAsyncAutocomplete = ({ const selectedItems = plainValue ? selected.map((s) => s.value) : selected.map((s) => - formatSelectedValue - ? formatSelectedValue(s) - : { id: parseInt(s.value), name: s.label } - ); + formatSelectedValue + ? formatSelectedValue(s) + : { id: parseInt(s.value), name: s.label } + ); helpers.setValue(selectedItems); }; @@ -99,7 +107,18 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={(e, newInput) => setSearchTerm(newInput)} + onInputChange={!localFilter ? (e, newInput) => setSearchTerm(newInput) : undefined} + filterOptions={ + // only apply filterOptions for "local" search + localFilter + ? (options, { inputValue }) => + options.filter((opt) => + String(opt.label ?? "").toLowerCase().includes( + String(inputValue ?? "").toLowerCase() + ) + ) + : undefined + } renderInput={(params) => ( (
  • - {multiple && } + {isMulti && } {option.label}
  • )} @@ -137,4 +156,13 @@ const MuiFormikAsyncAutocomplete = ({ ); }; +MuiFormikAsyncAutocomplete.propTypes = { + name: PropTypes.string.isRequired, + isMulti: PropTypes.bool, + queryFunction: PropTypes.func.isRequired, + formatOption: PropTypes.func, + queryParams: PropTypes.array, + localFilter: PropTypes.bool, +}; + export default MuiFormikAsyncAutocomplete; diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js new file mode 100644 index 00000000..aca9ac8e --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -0,0 +1,111 @@ +import React, { useEffect, useRef, useState } from "react"; +import PropTypes from "prop-types"; +import { Box, IconButton, InputAdornment, TextField } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import { useField } from "formik"; +import { DEBOUNCE_WAIT_150 } from "../../../utils/constants"; + +const MuiFormikColorInput = ({ name, placeholder = "Select a color", ...rest }) => { + const [field, meta, helpers] = useField(name); + const [hasValue, setHasValue] = useState(Boolean(field.value)); + const [localValue, setLocalValue] = useState(field.value || "#000000"); + const debounceRef = useRef(null); + + useEffect(() => { + setHasValue(Boolean(field.value)); + if (field.value && field.value !== localValue) setLocalValue(field.value); + }, [field.value]); + + useEffect(() => () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }, []); + + const handleChange = (e) => { + const value = e.target.value; + setLocalValue(value); + setHasValue(true); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + helpers.setValue(value); + debounceRef.current = null; + }, DEBOUNCE_WAIT_150); + }; + + const handleBlur = (e) => { + field.onBlur(e); + helpers.setTouched(true); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; + helpers.setValue(hasValue ? localValue : ""); + } + }; + + const handleClear = (e) => { + e.stopPropagation(); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; + } + setHasValue(false); + helpers.setValue(""); + helpers.setTouched(true); + }; + + return ( + + + + + + + ), + } : undefined} + sx={{ + "& input[type='color']::-webkit-color-swatch-wrapper": { padding: "2px" }, + }} + {...rest} + /> + {!hasValue && ( + + + {placeholder} + + + )} + + ); +}; + +MuiFormikColorInput.propTypes = { + name: PropTypes.string.isRequired, + placeholder: PropTypes.string +}; + +export default MuiFormikColorInput; diff --git a/src/utils/constants.js b/src/utils/constants.js index a70f1a95..b0c761c8 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -6,6 +6,7 @@ export const ZERO_INT = 0; export const CODE_200 = 200; +export const DEBOUNCE_WAIT_150 = 150; export const DEBOUNCE_WAIT_250 = 250; export const DEBOUNCE_WAIT = 500; diff --git a/webpack.common.js b/webpack.common.js index 6c198ca9..86ee01d1 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -107,6 +107,7 @@ module.exports = { 'components/mui/formik-inputs/async-select': './src/components/mui/formik-inputs/mui-formik-async-select.js', 'components/mui/formik-inputs/checkbox-group': './src/components/mui/formik-inputs/mui-formik-checkbox-group.js', 'components/mui/formik-inputs/checkbox': './src/components/mui/formik-inputs/mui-formik-checkbox.js', + 'components/mui/formik-inputs/color-input': './src/components/mui/formik-inputs/mui-formik-color-input.js', 'components/mui/formik-inputs/datepicker': './src/components/mui/formik-inputs/mui-formik-datepicker.js', 'components/mui/formik-inputs/discount-field': './src/components/mui/formik-inputs/mui-formik-discountfield.js', 'components/mui/formik-inputs/dropdown-checkbox': './src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js',