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 createPayload = (field, subtype, name) =>
|
subtype === 'post' ? { title: name, status: 'publish', postType: field.post_type } : { name };
|
|
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 canCreate = subtype === 'taxonomy';
|
console.log(field);
|
const buildPath = searchPath(field);
|
const ids = Array.isArray(value) ? value : value ? value.split(',') : [];
|
|
const [tokens, setTokens] = useState([]); // display strings
|
const [idByLabel, setIdByLabel] = useState({});
|
const [options, setOptions] = useState({}); // label -> id, from last search
|
const [search, setSearch] = useState('');
|
const [creating, setCreating] = useState(false);
|
|
const createLabel = search ? `Not found. Create "${search}"?` : null;
|
|
|
// 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) => {
|
setSearch(search);
|
if (!search) { setOptions({}); return;}
|
|
apiFetch({ path: buildPath(search) })
|
.then((results) => {
|
const map = {};
|
results.forEach((item) => { map[labelOf(item, subtype)] = item.id; });
|
setOptions(map);
|
})
|
.catch(() => {});
|
}, [buildPath, subtype]);
|
|
const commitIds = (newIds) => {
|
const isSingle = field.max === 1;
|
if (isSingle) {
|
onChange(newIds[newIds.length - 1] ?? 0);
|
} else {
|
onChange(field.max ? newIds.slice(0, field.max) : newIds);
|
}
|
};
|
|
const onChangeTokens = (newTokens) => {
|
const pendingCreate = newTokens.find((t) => t === createLabel);
|
|
if (pendingCreate && canCreate && !creating) {
|
setCreating(true);
|
apiFetch({ path: `/wp/v2/${field.taxonomy}`, method: 'POST', data: createPayload(field, subtype, search) })
|
.then((item) => {
|
const label = labelOf(item, subtype);
|
setIdByLabel((prev) => ({ ...prev, [label]: item.id }));
|
setTokens((prev) => {
|
const finalTokens = prev.filter((t) => t !== createLabel).concat(label);
|
const newIds = finalTokens.map((l) => idByLabel[l] ?? options[l] ?? (l === label ? item.id : undefined))
|
.filter((id) => id !== undefined);
|
commitIds(newIds);
|
return finalTokens;
|
});
|
})
|
.catch(() => {})
|
.finally(() => setCreating(false));
|
return;
|
}
|
|
const resolved = newTokens.filter(
|
(label) => idByLabel[label] !== undefined || options[label] !== undefined
|
);
|
const newIds = resolved.map((label) => idByLabel[label] ?? options[label]);
|
setTokens(resolved);
|
setIdByLabel((prev) => ({ ...prev, ...options }));
|
commitIds(newIds);
|
};
|
|
const noMatch = search && !Object.keys(options).some((l) => l.toLowerCase() === search.toLowerCase());
|
const suggestions = field.max === 1 && tokens.length
|
? []
|
: [...Object.keys(options), ...(canCreate && noMatch ? [createLabel] : [])];
|
|
|
return (
|
<FormTokenField
|
label={field.label}
|
help={field.description}
|
value={tokens}
|
suggestions={suggestions}
|
onChange={onChangeTokens}
|
onInputChange={onInputChange}
|
maxLength={field.max || undefined}
|
id={field.name}
|
__experimentalExpandOnFocus
|
/>
|
);
|
};
|
|
export default SelectorField;
|