From 70c218927de509913e8a6dac7fd1e66e2114b9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 30 Apr 2026 14:04:20 -0300 Subject: [PATCH 01/12] feat: add new mui formik color picker component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/mui-formik-color-input.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/components/mui/formik-inputs/mui-formik-color-input.js 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..1ead8266 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { TextField } from "@mui/material"; +import { useField } from "formik"; + +const MuiFormikColorInput = ({ name, ...rest }) => { + const [field, meta, helpers] = useField(name); + const [localValue, setLocalValue] = useState(field.value || "#000000"); + + const handleChange = (e) => { + setLocalValue(e.target.value); + }; + + const handleBlur = (e) => { + helpers.setValue(e.target.value); + helpers.setTouched(true); + }; + + return ( + + ); +}; + +MuiFormikColorInput.propTypes = { + name: PropTypes.string.isRequired +}; + +export default MuiFormikColorInput; From 98f0a7bca53e90ee4c957f5ea460d901b18953b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 30 Apr 2026 14:04:49 -0300 Subject: [PATCH 02/12] fix: add prop to fetch only initial options on async-select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui/formik-inputs/mui-formik-async-select.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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..55865b32 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -31,7 +31,8 @@ const MuiFormikAsyncAutocomplete = ({ formatOption = (item) => ({ value: item.id.toString(), label: item.name }), formatSelectedValue = null, queryParams = [], - isMulti = false + isMulti = false, + defaultOptions }) => { const [field, meta, helpers] = useField(name); const [options, setOptions] = useState([]); @@ -58,7 +59,7 @@ const MuiFormikAsyncAutocomplete = ({ }; useEffect(() => { - if (searchTerm) { + if (!defaultOptions && searchTerm) { const delayDebounce = setTimeout(() => { fetchOptions(searchTerm); }, DEBOUNCE_WAIT_250); @@ -99,7 +100,16 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={(e, newInput) => setSearchTerm(newInput)} + onInputChange={!defaultOptions ? (e, newInput) => setSearchTerm(newInput) : undefined} + filterOptions={ + // only apply filterOptions for "local" search + defaultOptions + ? (options, { inputValue }) => + options.filter((opt) => + opt.label.toLowerCase().includes(inputValue.toLowerCase()) + ) + : undefined + } renderInput={(params) => ( Date: Thu, 30 Apr 2026 15:16:26 -0300 Subject: [PATCH 03/12] fix: add entry point and export component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/index.js | 1 + webpack.common.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/index.js b/src/components/index.js index f32815eb..55f708d7 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -83,6 +83,7 @@ export {TotalRow as MuiTotalRow, NotesRow as MuiNotesRow, FeeRow as MuiFeeRow, P 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/webpack.common.js b/webpack.common.js index 2a9513fc..00192aea 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -108,6 +108,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', From 8de38299754120d5bef4ca2e0357710ef93091ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 30 Apr 2026 15:26:40 -0300 Subject: [PATCH 04/12] fix: add useEffect to sync data with formik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/formik-inputs/mui-formik-color-input.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 1ead8266..396d0199 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { TextField } from "@mui/material"; import { useField } from "formik"; @@ -7,6 +7,10 @@ const MuiFormikColorInput = ({ name, ...rest }) => { const [field, meta, helpers] = useField(name); const [localValue, setLocalValue] = useState(field.value || "#000000"); + useEffect(() => { + setLocalValue(field.value || "#000000"); + }, [field.value]); + const handleChange = (e) => { setLocalValue(e.target.value); }; From 21ef82cdd58710d7222384d5a53154a2b9f2a389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 5 May 2026 12:48:11 -0300 Subject: [PATCH 05/12] fix: adjust prop names and proptypes, color input ref and blur adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/mui-formik-async-select.js | 24 +++++++++++++++---- .../formik-inputs/mui-formik-color-input.js | 17 +++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) 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 55865b32..4db11e62 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -20,7 +20,15 @@ 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, @@ -32,7 +40,7 @@ const MuiFormikAsyncAutocomplete = ({ formatSelectedValue = null, queryParams = [], isMulti = false, - defaultOptions + localFilter = false }) => { const [field, meta, helpers] = useField(name); const [options, setOptions] = useState([]); @@ -59,7 +67,7 @@ const MuiFormikAsyncAutocomplete = ({ }; useEffect(() => { - if (!defaultOptions && searchTerm) { + if (!localFilter && searchTerm) { const delayDebounce = setTimeout(() => { fetchOptions(searchTerm); }, DEBOUNCE_WAIT_250); @@ -100,10 +108,10 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={!defaultOptions ? (e, newInput) => setSearchTerm(newInput) : undefined} + onInputChange={!localFilter ? (e, newInput) => setSearchTerm(newInput) : undefined} filterOptions={ // only apply filterOptions for "local" search - defaultOptions + localFilter ? (options, { inputValue }) => options.filter((opt) => opt.label.toLowerCase().includes(inputValue.toLowerCase()) @@ -147,4 +155,12 @@ const MuiFormikAsyncAutocomplete = ({ ); }; +MuiFormikAsyncAutocomplete.propTypes = { + name: PropTypes.string.isRequired, + 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 index 396d0199..9a71ed67 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useRef, useState } from "react"; import PropTypes from "prop-types"; import { TextField } from "@mui/material"; import { useField } from "formik"; @@ -6,23 +6,26 @@ import { useField } from "formik"; const MuiFormikColorInput = ({ name, ...rest }) => { const [field, meta, helpers] = useField(name); const [localValue, setLocalValue] = useState(field.value || "#000000"); - - useEffect(() => { - setLocalValue(field.value || "#000000"); - }, [field.value]); + const isDirtyRef = useRef(false); const handleChange = (e) => { setLocalValue(e.target.value); + isDirtyRef.current = true; }; const handleBlur = (e) => { - helpers.setValue(e.target.value); + field.onBlur(e); helpers.setTouched(true); + if (isDirtyRef.current) { + helpers.setValue(localValue); + isDirtyRef.current = false; + } }; return ( { fullWidth sx={{ "& input[type='color']::-webkit-color-swatch-wrapper": { - padding: "2px", + padding: "2px" } }} {...rest} From c800335192bc8874bf2363e67bf680f5d5ef2ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 6 May 2026 10:50:45 -0300 Subject: [PATCH 06/12] fix: add debounceRef to update formik value onChange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui/formik-inputs/mui-formik-color-input.js | 17 ++++++++++++----- src/utils/constants.js | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 9a71ed67..71b7c7e1 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -2,23 +2,30 @@ import React, { useRef, useState } from "react"; import PropTypes from "prop-types"; import { TextField } from "@mui/material"; import { useField } from "formik"; +import { DEBOUNCE_WAIT_150 } from "../../../utils/constants"; const MuiFormikColorInput = ({ name, ...rest }) => { const [field, meta, helpers] = useField(name); const [localValue, setLocalValue] = useState(field.value || "#000000"); - const isDirtyRef = useRef(false); + const debounceRef = useRef(null); const handleChange = (e) => { - setLocalValue(e.target.value); - isDirtyRef.current = true; + const value = e.target.value; + setLocalValue(value); + 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 (isDirtyRef.current) { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; helpers.setValue(localValue); - isDirtyRef.current = false; } }; diff --git a/src/utils/constants.js b/src/utils/constants.js index 85e190fc..3f140450 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -7,6 +7,7 @@ export const BPS = 100; export const CODE_200 = 200; +export const DEBOUNCE_WAIT_150 = 150; export const DEBOUNCE_WAIT_250 = 250; export const DEBOUNCE_WAIT = 500; From 66f427a6f4510b1cc6ea38ef7687bea50d4ce078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 8 May 2026 13:41:40 -0300 Subject: [PATCH 07/12] fix: address PR comments, change props and sync values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/mui-formik-async-select.js | 20 ++++++++++--------- .../formik-inputs/mui-formik-color-input.js | 11 +++++++++- 2 files changed, 21 insertions(+), 10 deletions(-) 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 4db11e62..13d72181 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -32,7 +32,6 @@ import PropTypes from "prop-types"; const MuiFormikAsyncAutocomplete = ({ name, queryFunction, - multiple = false, placeholder = "Select...", plainValue = false, hiddenOptions = [], @@ -47,7 +46,7 @@ const MuiFormikAsyncAutocomplete = ({ 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 = "") => { @@ -81,7 +80,7 @@ const MuiFormikAsyncAutocomplete = ({ }, []); const handleChange = (event, selected) => { - if (!multiple) { + if (!isMulti) { const selectedValue = plainValue ? selected?.value || "" : selected; helpers.setValue(selectedValue); return; @@ -90,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); }; @@ -114,7 +113,9 @@ const MuiFormikAsyncAutocomplete = ({ localFilter ? (options, { inputValue }) => options.filter((opt) => - opt.label.toLowerCase().includes(inputValue.toLowerCase()) + String(opt.label ?? "").toLowerCase().includes( + String(inputValue ?? "").toLowerCase() + ) ) : undefined } @@ -147,7 +148,7 @@ const MuiFormikAsyncAutocomplete = ({ )} renderOption={(props, option, { selected }) => (
  • - {multiple && } + {isMulti && } {option.label}
  • )} @@ -157,6 +158,7 @@ const MuiFormikAsyncAutocomplete = ({ MuiFormikAsyncAutocomplete.propTypes = { name: PropTypes.string.isRequired, + isMulti: PropTypes.bool, queryFunction: PropTypes.func.isRequired, formatOption: PropTypes.func, queryParams: PropTypes.array, diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 71b7c7e1..58fa18a7 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { TextField } from "@mui/material"; import { useField } from "formik"; @@ -9,6 +9,15 @@ const MuiFormikColorInput = ({ name, ...rest }) => { const [localValue, setLocalValue] = useState(field.value || "#000000"); const debounceRef = useRef(null); + useEffect(() => { + if (field.value !== localValue) setLocalValue(field.value || "#000000"); + }, [field.value]); + + useEffect(() => () => { + if (!field.value) helpers.setValue("#000000"); + if (debounceRef.current) clearTimeout(debounceRef.current); + }, []); + const handleChange = (e) => { const value = e.target.value; setLocalValue(value); From efd3bc555a80295bf8389d9278169ff8d4b4b9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 13 May 2026 01:09:42 -0300 Subject: [PATCH 08/12] fix: adjust color input, display no color selected, allow to clear selected value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/mui-formik-color-input.js | 86 ++++++++++++++----- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 58fa18a7..491b43b7 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -1,26 +1,29 @@ import React, { useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; -import { TextField } from "@mui/material"; +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, ...rest }) => { +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(() => { - if (field.value !== localValue) setLocalValue(field.value || "#000000"); + setHasValue(Boolean(field.value)); + if (field.value && field.value !== localValue) setLocalValue(field.value); }, [field.value]); useEffect(() => () => { - if (!field.value) helpers.setValue("#000000"); 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); @@ -34,32 +37,71 @@ const MuiFormikColorInput = ({ name, ...rest }) => { if (debounceRef.current) { clearTimeout(debounceRef.current); debounceRef.current = null; - helpers.setValue(localValue); + helpers.setValue(hasValue ? localValue : null); } }; + const handleClear = (e) => { + e.stopPropagation(); + 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 + name: PropTypes.string.isRequired, + placeholder: PropTypes.string }; export default MuiFormikColorInput; From af2bf3472a85b2cd699839dfeba236a341c44f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 13 May 2026 01:27:16 -0300 Subject: [PATCH 09/12] fix: adjust empty value, clear debounce on handleClear function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/formik-inputs/mui-formik-color-input.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 491b43b7..aca9ac8e 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -37,12 +37,16 @@ const MuiFormikColorInput = ({ name, placeholder = "Select a color", ...rest }) if (debounceRef.current) { clearTimeout(debounceRef.current); debounceRef.current = null; - helpers.setValue(hasValue ? localValue : 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); From 9b62075b95acce8fa9e447c90423d21658782f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 20 May 2026 11:27:08 -0300 Subject: [PATCH 10/12] fix: color picker input props merging, replace undefined on inputChange with pass through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/mui-formik-async-select.js | 2 +- .../formik-inputs/mui-formik-color-input.js | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) 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 13d72181..e0997957 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -107,7 +107,7 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={!localFilter ? (e, newInput) => setSearchTerm(newInput) : undefined} + onInputChange={!localFilter ? (e, newInput) => setSearchTerm(newInput) : (x) => x} filterOptions={ // only apply filterOptions for "local" search localFilter diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index aca9ac8e..6673bc26 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -5,7 +5,7 @@ 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 MuiFormikColorInput = ({ name, placeholder = "Select a color", InputProps: restInputProps, ...rest }) => { const [field, meta, helpers] = useField(name); const [hasValue, setHasValue] = useState(Boolean(field.value)); const [localValue, setLocalValue] = useState(field.value || "#000000"); @@ -14,7 +14,7 @@ const MuiFormikColorInput = ({ name, placeholder = "Select a color", ...rest }) useEffect(() => { setHasValue(Boolean(field.value)); if (field.value && field.value !== localValue) setLocalValue(field.value); - }, [field.value]); + }, [field.value, localValue]); useEffect(() => () => { if (debounceRef.current) clearTimeout(debounceRef.current); @@ -63,15 +63,17 @@ const MuiFormikColorInput = ({ name, placeholder = "Select a color", ...rest }) error={meta.touched && Boolean(meta.error)} helperText={meta.touched && meta.error} fullWidth - InputProps={hasValue ? { - endAdornment: ( - - - - - - ), - } : undefined} + InputProps={{ + ...(hasValue ? { + endAdornment: ( + + + + + + ), + } : {}), ...restInputProps + }} sx={{ "& input[type='color']::-webkit-color-swatch-wrapper": { padding: "2px" }, }} From 80fd6e0959b8d9df5dfd50e746aa5ba00f58688c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 27 May 2026 16:47:03 -0300 Subject: [PATCH 11/12] fix: revert async automcomplete changes, use i18n for literals --- .../formik-inputs/mui-formik-async-select.js | 52 +++++-------------- .../formik-inputs/mui-formik-color-input.js | 7 +-- src/i18n/en.json | 3 ++ 3 files changed, 19 insertions(+), 43 deletions(-) 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 e0997957..109c1ddc 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -20,33 +20,25 @@ 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, - localFilter = false + isMulti = false }) => { const [field, meta, helpers] = useField(name); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const value = field.value || (isMulti ? [] : null); + const value = field.value || (multiple ? [] : null); const error = meta.touched && meta.error; const fetchOptions = async (input = "") => { @@ -66,7 +58,7 @@ const MuiFormikAsyncAutocomplete = ({ }; useEffect(() => { - if (!localFilter && searchTerm) { + if (searchTerm) { const delayDebounce = setTimeout(() => { fetchOptions(searchTerm); }, DEBOUNCE_WAIT_250); @@ -80,7 +72,7 @@ const MuiFormikAsyncAutocomplete = ({ }, []); const handleChange = (event, selected) => { - if (!isMulti) { + if (!multiple) { const selectedValue = plainValue ? selected?.value || "" : selected; helpers.setValue(selectedValue); return; @@ -89,10 +81,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); }; @@ -107,18 +99,7 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={!localFilter ? (e, newInput) => setSearchTerm(newInput) : (x) => x} - filterOptions={ - // only apply filterOptions for "local" search - localFilter - ? (options, { inputValue }) => - options.filter((opt) => - String(opt.label ?? "").toLowerCase().includes( - String(inputValue ?? "").toLowerCase() - ) - ) - : undefined - } + onInputChange={(e, newInput) => setSearchTerm(newInput)} renderInput={(params) => ( (
  • - {isMulti && } + {multiple && } {option.label}
  • )} @@ -156,13 +137,4 @@ 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; +export default MuiFormikAsyncAutocomplete; \ No newline at end of file diff --git a/src/components/mui/formik-inputs/mui-formik-color-input.js b/src/components/mui/formik-inputs/mui-formik-color-input.js index 6673bc26..20404ad0 100644 --- a/src/components/mui/formik-inputs/mui-formik-color-input.js +++ b/src/components/mui/formik-inputs/mui-formik-color-input.js @@ -1,11 +1,12 @@ import React, { useEffect, useRef, useState } from "react"; +import T from "i18n-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"; +import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; -const MuiFormikColorInput = ({ name, placeholder = "Select a color", InputProps: restInputProps, ...rest }) => { +const MuiFormikColorInput = ({ name, placeholder = T.translate("color_picker.placeholder"), InputProps: restInputProps, ...rest }) => { const [field, meta, helpers] = useField(name); const [hasValue, setHasValue] = useState(Boolean(field.value)); const [localValue, setLocalValue] = useState(field.value || "#000000"); @@ -28,7 +29,7 @@ const MuiFormikColorInput = ({ name, placeholder = "Select a color", InputProps: debounceRef.current = setTimeout(() => { helpers.setValue(value); debounceRef.current = null; - }, DEBOUNCE_WAIT_150); + }, DEBOUNCE_WAIT_250); }; const handleBlur = (e) => { diff --git a/src/i18n/en.json b/src/i18n/en.json index 01f7bf27..96f2f2f8 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -138,5 +138,8 @@ "total": "Total", "rate": "Rate", "action": "Action" + }, + "color_picker": { + "placeholder": "Select a color" } } From 8bbad9020988a086bf87ce337f4336680703f318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 28 May 2026 14:58:37 -0300 Subject: [PATCH 12/12] fix: add unit tests for mui formik color input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/mui-formik-color-input.test.js | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/components/mui/__tests__/mui-formik-color-input.test.js diff --git a/src/components/mui/__tests__/mui-formik-color-input.test.js b/src/components/mui/__tests__/mui-formik-color-input.test.js new file mode 100644 index 00000000..889838c2 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-color-input.test.js @@ -0,0 +1,166 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikColorInput from "../formik-inputs/mui-formik-color-input"; + +const DEBOUNCE_MS = 250; +const PLACEHOLDER_KEY = "color_picker.placeholder"; + +const renderWithFormik = (props, initialValues = { testField: "" }) => + render( + +
    + + +
    + ); + +const renderWithSubmit = (initialValues = { testField: "" }, props = {}) => { + const onSubmit = jest.fn(); + render( + +
    + + + +
    + ); + return onSubmit; +}; + +// Restore real timers after each test to prevent leaking fake timers +afterEach(() => jest.useRealTimers()); + +const getColorInput = () => document.querySelector('input[type="color"]'); + +describe("MuiFormikColorInput", () => { + test("renders a color input", () => { + renderWithFormik({}); + expect(getColorInput()).toBeInTheDocument(); + }); + + test("loads initial value: reflects value, shows clear button, hides placeholder", () => { + renderWithFormik({}, { testField: "#ff0000" }); + expect(getColorInput()).toHaveValue("#ff0000"); + expect(screen.getByRole("button")).toBeInTheDocument(); + expect(screen.queryByText(PLACEHOLDER_KEY)).not.toBeInTheDocument(); + }); + + test("empty initial state: shows placeholder and no clear button", () => { + renderWithFormik({}); + expect(screen.getByText(PLACEHOLDER_KEY)).toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + test("blur without selection: formik value stays empty and placeholder remains", async () => { + const onSubmit = renderWithSubmit({ testField: "" }); + const input = getColorInput(); + + fireEvent.focus(input); + fireEvent.blur(input); + + expect(screen.getByText(PLACEHOLDER_KEY)).toBeInTheDocument(); + fireEvent.click(screen.getByText("Submit")); + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith({ testField: "" }, expect.anything()); + }); + }); + + // The useEffect resets hasValue to false whenever localValue changes (before the debounce + // commits the new color to field.value). The clear button only appears after the debounce fires. + test("select a color: shows clear button and hides placeholder after debounce", () => { + jest.useFakeTimers(); + renderWithFormik({}); + + fireEvent.change(getColorInput(), { target: { value: "#33aaff" } }); + act(() => jest.advanceTimersByTime(DEBOUNCE_MS)); + + expect(screen.getByRole("button")).toBeInTheDocument(); + expect(screen.queryByText(PLACEHOLDER_KEY)).not.toBeInTheDocument(); + }); + + test("select a color: updates formik value after debounce", async () => { + jest.useFakeTimers(); + const onSubmit = renderWithSubmit({ testField: "" }); + + fireEvent.change(getColorInput(), { target: { value: "#33aaff" } }); + act(() => jest.advanceTimersByTime(DEBOUNCE_MS)); + jest.useRealTimers(); + + fireEvent.click(screen.getByText("Submit")); + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith({ testField: "#33aaff" }, expect.anything()); + }); + }); + + test("clear button: shows placeholder and hides clear button", () => { + renderWithFormik({}, { testField: "#ff0000" }); + + fireEvent.click(screen.getByRole("button")); + + expect(screen.getByText(PLACEHOLDER_KEY)).toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + test("clear button: resets formik value to empty", async () => { + const onSubmit = renderWithSubmit({ testField: "#ff0000" }); + + fireEvent.click(screen.getAllByRole("button")[0]); + + fireEvent.click(screen.getByText("Submit")); + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith({ testField: "" }, expect.anything()); + }); + }); + + test("shows error message when field is touched and has an error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Color is required")).toBeInTheDocument(); + }); + + test("does not show error when field is not yet touched", () => { + render( + +
    + + +
    + ); + expect(screen.queryByText("Color is required")).not.toBeInTheDocument(); + }); +});