import {useState, useEffect, useMemo} from '@wordpress/element'; import {Spinner} from '@wordpress/components'; import FieldControl from '../components/FieldControl'; import {addQueryArgs} from '@wordpress/url'; import TokenSelect from '../components/TokenSelect'; const useDebounce = ( value, delay ) => { const [debouncedValue, setDebouncedValue] = useState( value ); useEffect( () => { const handler = setTimeout( () => setDebouncedValue( value ), delay ); return () => clearTimeout( handler ); }, [value, delay] ); return debouncedValue; }; const TokenSelectField = ( {field, value, onChange, variantClass, tokenClassName} ) => { const isValueEmpty = value === undefined || value === null || value === ''; const disabled = (field.premium || field.disabled) ?? false; useEffect( () => { if ( isValueEmpty && field.default !== undefined ) { onChange( field.id, field.default ); } }, [isValueEmpty, field.default, field.id, onChange] ); const currentSelectionIds = useMemo( () => { if ( value === null || value === undefined ) { return []; } return Array.isArray( value ) ? value : [value]; }, [value] ); // States for the data logic const [searchQuery, setSearchQuery] = useState( '' ); const debouncedSearchQuery = useDebounce( searchQuery, 300 ); const [availableOptions, setAvailableOptions] = useState( field.options || [] ); const [isLoading, setIsLoading] = useState( false ); // Load the dynamic options useEffect( () => { if ( !field.options_callback?.rest_endpoint ) { return; } const abortController = new AbortController(); const fetchOptions = async () => { setIsLoading( true ); const endpointPath = field.options_callback.rest_endpoint; const queryArgs = {}; if ( debouncedSearchQuery ) { queryArgs.search = debouncedSearchQuery; } // Ensure the already-selected value(s) are included if ( currentSelectionIds.length > 0 ) { queryArgs.include = currentSelectionIds; } const finalUrl = addQueryArgs( endpointPath, queryArgs ); try { const options = await wp.apiFetch( { path: finalUrl, signal: abortController.signal } ); setAvailableOptions( options ); } catch ( error ) { if ( error.name !== 'AbortError' ) { console.error( `Failed to fetch options from '${endpointPath}'.`, error ); } } finally { setIsLoading( false ); } }; fetchOptions(); return () => abortController.abort(); }, [debouncedSearchQuery, field.options_callback, currentSelectionIds] ); const selectedOptions = useMemo( () => { return currentSelectionIds.map( id => { // Search in flat list let found = availableOptions.find( opt => opt.value == id ); // Search in groups if ( !found ) { for ( const group of availableOptions ) { if ( group.options && Array.isArray( group.options ) ) { found = group.options.find( opt => opt.value == id ); if ( found ) { break; } } } } return found; } ).filter( Boolean ); // Remove not found options }, [currentSelectionIds, availableOptions] ); const handleSelectionChange = ( newSelectedOptions ) => { if ( !newSelectedOptions ) { onChange( field.id, field.isSingle ? '' : [] ); return; } const newIds = newSelectedOptions.map( opt => opt.value ); // For single select save the value, for multi the array const valueToSave = field.isSingle ? (newIds[0] || '') : newIds; onChange( field.id, valueToSave ); }; return ( ); }; export default TokenSelectField;