import { useState, useEffect, useCallback } from '@wordpress/element'; import { FormTokenField } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; const resolveSubtype = (field) => field.subtype || field.type; // 'taxonomy' | 'user' | 'post' const searchPath = (field) => { const subtype = resolveSubtype(field); if (subtype === 'user') { return (search) => addQueryArgs('/wp/v2/users', { search, per_page: 20 }); } if (subtype === 'taxonomy') { // Assumes taxonomy rest_base matches the (unprefixed) taxonomy slug. return (search) => addQueryArgs(`/wp/v2/${field.taxonomy}`, { search, per_page: 20 }); } return (search) => addQueryArgs('/wp/v2/search', { search, type: 'post', subtype: field.post_type, per_page: 20 }); }; const labelOf = (item, subtype) => (subtype === 'user' ? item.name : item.title?.rendered ?? item.title ?? item.name); const SelectorField = ({ field, value, onChange }) => { const subtype = resolveSubtype(field); const buildPath = searchPath(field); const ids = Array.isArray(value) ? value : value ? [value] : []; const [tokens, setTokens] = useState([]); // display strings const [idByLabel, setIdByLabel] = useState({}); const [options, setOptions] = useState({}); // label -> id, from last search // Resolve current IDs to labels on mount / value change useEffect(() => { let cancelled = false; if (!ids.length) { setTokens([]); return; } Promise.all(ids.map((id) => apiFetch({ path: buildPath('') + `&include=${id}` }))) .then((results) => { if (cancelled) return; const map = {}; const labels = []; results.flat().forEach((item) => { const label = labelOf(item, subtype); map[label] = item.id; labels.push(label); }); setIdByLabel((prev) => ({ ...prev, ...map })); setTokens(labels); }) .catch(() => {}); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(ids)]); const onInputChange = useCallback((search) => { if (!search) return; apiFetch({ path: buildPath(search) }) .then((results) => { const map = {}; results.forEach((item) => { map[labelOf(item, subtype)] = item.id; }); setOptions(map); }) .catch(() => {}); }, [buildPath, subtype]); const onChangeTokens = (newTokens) => { setTokens(newTokens); const newIds = newTokens .map((label) => idByLabel[label] ?? options[label]) .filter((id) => id !== undefined); setIdByLabel((prev) => ({ ...prev, ...options })); const isSingle = field.max === 1; if (isSingle) { onChange(newIds[newIds.length - 1] ?? 0); } else { onChange(field.max ? newIds.slice(0, field.max) : newIds); } }; return ( ); }; export default SelectorField;