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 (
|
<FormTokenField
|
label={field.label}
|
help={field.description}
|
value={tokens}
|
suggestions={field.max === 1 && tokens.length ? [] : Object.keys(options)}
|
onChange={onChangeTokens}
|
onInputChange={onInputChange}
|
maxLength={field.max || undefined}
|
__experimentalExpandOnFocus
|
/>
|
);
|
};
|
|
export default SelectorField;
|