-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add new mui formik color picker component #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3f4737c
c31bbc0
30a7871
ab8685a
ae683f6
5241fff
aafe693
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(() => { | ||
|
tomrndom marked this conversation as resolved.
|
||
| if (searchTerm) { | ||
| if (!localFilter && searchTerm) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] The new docblock promises "fetches once on mount" + client-side filtering, but the only mount-time call ( Suggested fix: in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be set on the implementation as part of the queryParams according to the queryFunction used. Since the order of the per_page parameter is not always in the same position is a bit difficult to use a default MAX_PAGE_SIZE for all queryActions |
||
| 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() | ||
| ) | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| : undefined | ||
| } | ||
| renderInput={(params) => ( | ||
| <TextField | ||
| {...params} | ||
|
|
@@ -129,12 +148,21 @@ const MuiFormikAsyncAutocomplete = ({ | |
| )} | ||
| renderOption={(props, option, { selected }) => ( | ||
| <li {...props}> | ||
| {multiple && <Checkbox checked={selected} sx={{ mr: 1 }} />} | ||
| {isMulti && <Checkbox checked={selected} sx={{ mr: 1 }} />} | ||
| {option.label} | ||
| </li> | ||
| )} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import React, { useEffect, 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"); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Default
Suggested fix: also seed Formik on mount when useEffect(() => { if (!field.value) helpers.setValue("#000000"); }, []);or render the swatch only when
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The color picker would pick a default color if the user don't interact with the component. It looks like this contradicts #231 (comment). |
||
| const debounceRef = useRef(null); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] No re-sync with external
Suggested fix: useEffect(() => {
if (field.value !== localValue) setLocalValue(field.value || "#000000");
}, [field.value]);(guard against overwriting in-flight typing if needed.) [LOW] No debounce cleanup on unmount. A pending useEffect(() => () => { if (debounceRef.current) clearTimeout(debounceRef.current); }, []); |
||
|
|
||
| useEffect(() => { | ||
| if (field.value !== localValue) setLocalValue(field.value || "#000000"); | ||
| }, [field.value]); | ||
|
|
||
| useEffect(() => () => { | ||
| if (!field.value) helpers.setValue("#000000"); | ||
| if (debounceRef.current) clearTimeout(debounceRef.current); | ||
| }, []); | ||
|
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/components/mui/formik-inputs/mui-formik-color-input.jsRepository: OpenStackweb/openstack-uicore-foundation Length of output: 2319 Remove Formik writes from unmount cleanup (stale-value side effect). On line 17, the cleanup callback captures The debounce timeout cleanup on line 18 is necessary and should remain, but the conditional write is redundant— Proposed fix- useEffect(() => () => {
- if (!field.value) helpers.setValue("#000000");
- if (debounceRef.current) clearTimeout(debounceRef.current);
- }, []);
+ useEffect(
+ () => () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ },
+ []
+ );🤖 Prompt for AI Agents |
||
|
|
||
| const handleChange = (e) => { | ||
| 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 (debounceRef.current) { | ||
| clearTimeout(debounceRef.current); | ||
| debounceRef.current = null; | ||
| helpers.setValue(localValue); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <TextField | ||
| type="color" | ||
| name={field.name} | ||
| value={localValue} | ||
| onChange={handleChange} | ||
| onBlur={handleBlur} | ||
|
tomrndom marked this conversation as resolved.
|
||
| error={meta.touched && Boolean(meta.error)} | ||
| helperText={meta.touched && meta.error} | ||
| fullWidth | ||
| sx={{ | ||
| "& input[type='color']::-webkit-color-swatch-wrapper": { | ||
| padding: "2px" | ||
| } | ||
| }} | ||
| {...rest} | ||
| /> | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ); | ||
| }; | ||
|
|
||
| MuiFormikColorInput.propTypes = { | ||
| name: PropTypes.string.isRequired | ||
| }; | ||
|
|
||
| export default MuiFormikColorInput; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM] Pre-existing
multiplevsisMultidual-prop divergence (worth fixing while editing this file).multiple(defaultfalse, line 35) drives thehandleChangevalue-shape branch (single vs array).multiple={isMulti}(further down) is what's passed to MUIAutocomplete.A consumer passing only
isMulti={true}(withoutmultiple={true}) gets multi-select UI with single-value Formik writes → array/object collisions on submit. The newpropTypesblock cements this two-name API into the public surface.Suggested fix: collapse to one prop (
multiple), or alias one to the other in destructuring (multiple = isMulti), and update propTypes accordingly.