From 302373e19c6981401e901f094a268e7c6f3c8bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 29 Oct 2026 14:55:44 +0100 Subject: [PATCH 1/7] basic refactor --- client/components/combobox.jsx | 200 ++++++++++++++++----------------- 1 file changed, 97 insertions(+), 103 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 8f6f4ead9..036494224 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,117 +1,113 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState, useEffect, useRef } from 'react'; import _ from 'lodash'; import './combobox.less'; -const Combobox = createReactClass({ - displayName : 'Combobox', - getDefaultProps : function() { - return { - id : '', - className : '', - trigger : 'hover', - default : '', - placeholder : '', - tooltip : '', - autoSuggest : { - clearAutoSuggestOnClick : true, - suggestMethod : 'includes', - filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter - }, - valuePatterns : /.+/ - }; +const Combobox = ({ + id = '', + className = '', + trigger = 'hover', + default: defaultValue = '', + placeholder = '', + tooltip = '', + autoSuggest = { + clearAutoSuggestOnClick : true, + suggestMethod : 'includes', + filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, - getInitialState : function() { - this.dropdownRef = React.createRef(); - return { - showDropdown : false, - value : '', - options : [...this.props.options], - inputFocused : false - }; - }, - componentDidMount : function() { - if(this.props.trigger == 'click') - document.addEventListener('click', this.handleClickOutside); - this.setState({ - value : this.props.default - }); - }, - componentWillUnmount : function() { - if(this.props.trigger == 'click') - document.removeEventListener('click', this.handleClickOutside); - }, - handleClickOutside : function(e){ - // Close dropdown when clicked outside - if(this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) { - this.handleDropdown(false); + valuePatterns = /.+/, + ...props +})=>{ + + const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + const [value, setValue] = useState(''); + const [options, setOptions] = useState([...props.options]); + const [inputFocused, setInputFocused] = useState(false); + + + useEffect(()=>{ + if(trigger == 'click') { + document.addEventListener('click', handleClickOutside); } - }, - handleDropdown : function(show){ - this.setState({ - value : show ? '' : this.props.default, - showDropdown : show, - inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false - }); - }, - handleInput : function(e){ - e.persist(); - this.setState({ - value : e.target.value, - inputFocused : false - }, ()=>{ - this.props.onEntry(e); - }); - }, - handleSelect : function(value, data=value){ - this.setState({ - value : value - }, ()=>{this.props.onSelect(data);}); - ; - }, - renderTextInput : function(){ + setValue(defaultValue); + + return ()=>{ + if(trigger == 'click') { + document.removeEventListener('click', handleClickOutside); + } + }; + }, []); + + const handleClickOutside = (e)=>{ + // Close dropdown when clicked outside + if(dropdownRef.current && !dropdownRef.current.contains(e.target)) { + handleDropdown(false); + } + }; + + const handleDropdown = (show)=>{ + setValue(show ? '' : defaultValue); + setShowDropdown(show); + setInputFocused(autoSuggest.clearAutoSuggestOnClick ? show : false); + }; + + const handleInput = (e)=>{ + setValue(e.target.value); + setInputFocused(false); + props.onEntry(e); + }; + + const handleSelect = (value, data=value)=>{ + setValue(value); + props.onSelect(data); + }; + + const renderTextInput = ()=>{ return (
{this.handleDropdown(true);} : undefined} - onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined} - {...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}> + onMouseEnter={trigger == 'hover' ? ()=>{handleDropdown(true);} : undefined} + onClick= {trigger == 'click' ? ()=>{handleDropdown(true);} : undefined} + {...(tooltip ? { 'data-tooltip-right': tooltip } : {})}> this.handleInput(e)} - value={this.state.value || ''} + onChange={(e)=>handleInput(e)} + value={value || ''} title='' - pattern={this.props.valuePatterns} - placeholder={this.props.placeholder} + pattern={valuePatterns} + placeholder={placeholder} onBlur={(e)=>{ if(!e.target.checkValidity()){ - this.setState({ - value : this.props.default - }); + setValue(defaultValue); } }} onKeyDown={(e)=>{ if(e.key === 'Enter') { e.preventDefault(); - this.props.onEntry(e); + props.onEntry(e); } }} />
); - }, - renderDropdown : function(dropdownChildren){ - if(!this.state.showDropdown) return null; - if(this.props.autoSuggest && !this.state.inputFocused){ - const suggestMethod = this.props.autoSuggest.suggestMethod; - const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn; + }; + + const renderDropdown = ()=>{ + if(!showDropdown) return null; + const dropdownChildren = options.map((child, i)=>{ + const clone = React.cloneElement(child, { onClick: ()=>handleSelect(child.props.value, child.props.data) }); + return clone; + }); + if(autoSuggest && !inputFocused){ + const suggestMethod = autoSuggest.suggestMethod; + const filterOn = _.isString(autoSuggest.filterOn) ? [autoSuggest.filterOn] : autoSuggest.filterOn; const filteredArrays = filterOn.map((attr)=>{ const children = dropdownChildren.filter((item)=>{ if(suggestMethod === 'includes') - return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase()); + return item.props[attr]?.toLowerCase().includes(value.toLowerCase()); if(suggestMethod === 'startsWith') - return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase()); + return item.props[attr]?.toLowerCase().startsWith(value.toLowerCase()); }); return children; }); @@ -123,21 +119,19 @@ const Combobox = createReactClass({ {dropdownChildren} ); - }, - render : function () { - const dropdownChildren = this.state.options.map((child, i)=>{ - const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) }); - return clone; - }); - return ( -
{this.handleDropdown(false);} : undefined}> - {this.renderTextInput()} - {this.renderDropdown(dropdownChildren)} -
- ); - } -}); + }; + + + return ( +
{handleDropdown(false);} : undefined}> + {renderTextInput()} + {renderDropdown()} +
+ ); + + +}; export default Combobox; From 8ee317e8c4e6fb7aab77e4d4dfb5a2df3c5c89b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 29 Oct 2026 14:56:35 +0100 Subject: [PATCH 2/7] linting --- client/components/combobox.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 036494224..1bb36d4a3 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -24,7 +24,6 @@ const Combobox = ({ const [options, setOptions] = useState([...props.options]); const [inputFocused, setInputFocused] = useState(false); - useEffect(()=>{ if(trigger == 'click') { document.addEventListener('click', handleClickOutside); @@ -121,7 +120,6 @@ const Combobox = ({ ); }; - return (
); - }; export default Combobox; From f38d20540fa9c76b121e805fdb5c1eb4e31e7974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 29 Oct 2026 15:05:44 +0100 Subject: [PATCH 3/7] simplify as possible --- client/components/combobox.jsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 1bb36d4a3..497634c19 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -15,33 +15,24 @@ const Combobox = ({ filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, valuePatterns = /.+/, + options, ...props })=>{ - const dropdownRef = useRef(null); const [showDropdown, setShowDropdown] = useState(false); - const [value, setValue] = useState(''); - const [options, setOptions] = useState([...props.options]); const [inputFocused, setInputFocused] = useState(false); + const [value, setValue] = useState(defaultValue); useEffect(()=>{ - if(trigger == 'click') { + if(trigger === 'click') { document.addEventListener('click', handleClickOutside); + return ()=>document.removeEventListener('click', handleClickOutside); } - setValue(defaultValue); - - return ()=>{ - if(trigger == 'click') { - document.removeEventListener('click', handleClickOutside); - } - }; }, []); const handleClickOutside = (e)=>{ // Close dropdown when clicked outside - if(dropdownRef.current && !dropdownRef.current.contains(e.target)) { - handleDropdown(false); - } + if(!dropdownRef.current?.contains(e.target)) handleDropdown(false); }; const handleDropdown = (show)=>{ @@ -112,7 +103,6 @@ const Combobox = ({ }); dropdownChildren = _.uniq(filteredArrays.flat(1)); } - return (
{dropdownChildren} From 4ec73938b425f8bcd8b96dbb81d7fe243e834150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 29 Oct 2026 15:08:11 +0100 Subject: [PATCH 4/7] further simplification --- client/components/combobox.jsx | 4 ++-- client/homebrew/editor/metadataEditor/metadataEditor.jsx | 4 ++-- client/homebrew/editor/tagInput/tagInput.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 497634c19..5f160d112 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -6,7 +6,7 @@ const Combobox = ({ id = '', className = '', trigger = 'hover', - default: defaultValue = '', + defaultValue = '', placeholder = '', tooltip = '', autoSuggest = { @@ -15,7 +15,7 @@ const Combobox = ({ filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, valuePatterns = /.+/, - options, + options = [], ...props })=>{ const dropdownRef = useRef(null); diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index d5c4387e9..518cc0ac8 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -219,7 +219,7 @@ const MetadataEditor = ({ handleTheme(value)} onEntry={(e)=>{ @@ -261,7 +261,7 @@ const MetadataEditor = ({ handleLanguage(value)} onEntry={(e)=>{ diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index a153fef8b..ede194834 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -131,7 +131,7 @@ const TagInput = ({ id, tooltip, label, valuePatterns, values = [], unique = tru id={id} trigger='click' className='tagInput-dropdown' - default='' + defaultValue='' placeholder={placeholder} options={label === 'tags' ? suggestionOptions : []} tooltip={tooltip} From 9b02d74fb1e58f619d6012406e0f05f56e93f506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 29 Oct 2026 15:13:05 +0100 Subject: [PATCH 5/7] fix build --- client/components/combobox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 5f160d112..f78cbe9a2 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -85,7 +85,7 @@ const Combobox = ({ const renderDropdown = ()=>{ if(!showDropdown) return null; - const dropdownChildren = options.map((child, i)=>{ + let dropdownChildren = options.map((child, i)=>{ const clone = React.cloneElement(child, { onClick: ()=>handleSelect(child.props.value, child.props.data) }); return clone; }); From 89667f38b5a0e862f03cb8c78f9d90d28c2ea4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 30 Oct 2026 00:16:07 +0100 Subject: [PATCH 6/7] fix issue, --- client/components/combobox.jsx | 21 +++++++++++++------ .../editor/metadataEditor/metadataEditor.jsx | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index f78cbe9a2..c89ccb908 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import _ from 'lodash'; import './combobox.less'; @@ -16,6 +16,7 @@ const Combobox = ({ }, valuePatterns = /.+/, options = [], + keepValueOnClose = false, ...props })=>{ const dropdownRef = useRef(null); @@ -30,15 +31,23 @@ const Combobox = ({ } }, []); - const handleClickOutside = (e)=>{ + const handleClickOutside = useCallback((e)=>{ // Close dropdown when clicked outside - if(!dropdownRef.current?.contains(e.target)) handleDropdown(false); - }; + if(!dropdownRef.current?.contains(e.target)) { + handleDropdown(false); + } + }, []); const handleDropdown = (show)=>{ - setValue(show ? '' : defaultValue); + if(show) { + if(autoSuggest.clearAutoSuggestOnClick) { + setValue(''); + } + } else if(!keepValueOnClose) { + setValue(defaultValue); + } setShowDropdown(show); - setInputFocused(autoSuggest.clearAutoSuggestOnClick ? show : false); + setInputFocused(autoSuggest.clearAutoSuggestOnClick && show); }; const handleInput = (e)=>{ diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 518cc0ac8..54332b468 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -262,6 +262,7 @@ const MetadataEditor = ({ id='combobox-language' className='language-dropdown' defaultValue={metadata.lang || ''} + keepValueOnClose={true} placeholder='en' onSelect={(value)=>handleLanguage(value)} onEntry={(e)=>{ From f5056049eb4be0e8afa111cfa70d6b3c9caed786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 30 Oct 2026 00:30:44 +0100 Subject: [PATCH 7/7] add proper pattern validation --- client/components/combobox.jsx | 2 +- client/homebrew/editor/metadataEditor/metadataEditor.jsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index c89ccb908..230d079db 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -14,7 +14,7 @@ const Combobox = ({ suggestMethod : 'includes', filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, - valuePatterns = /.+/, + valuePatterns = '.+', options = [], keepValueOnClose = false, ...props diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 54332b468..feb1cf2b1 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -264,6 +264,7 @@ const MetadataEditor = ({ defaultValue={metadata.lang || ''} keepValueOnClose={true} placeholder='en' + valuePatterns="^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$" onSelect={(value)=>handleLanguage(value)} onEntry={(e)=>{ e.target.setCustomValidity(''); //Clear the validation popup while typing