/**
* WordPress dependencies
*/
import {
__experimentalFetchLinkSuggestions as fetchLinkSuggestions,
store as coreStore,
} from '@wordpress/core-data';
import { Button, ComboboxControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, createInterpolateElement } from '@wordpress/element';
import { debounce } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import type { DataFormControlProps } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
import type { MediaItem } from '../types';
import { getRenderedContent } from '../utils/get-rendered-content';
export type SearchResult = {
/**
* Post or term id.
*/
id: number;
/**
* Link url.
*/
url: string;
/**
* Title of the link.
*/
title: string;
/**
* The taxonomy or post type slug or type URL.
*/
type: string;
/**
* Link kind of post-type or taxonomy
*/
kind?: string;
};
export default function MediaAttachedToEdit( {
data,
onChange,
}: DataFormControlProps< MediaItem > ) {
const defaultPost =
!! data.post && !! data?._embedded?.[ 'wp:attached-to' ]?.[ 0 ]
? [
{
label: getRenderedContent(
data._embedded?.[ 'wp:attached-to' ]?.[ 0 ]?.title
),
value: data.post.toString(),
},
]
: [];
const [ options, setOptions ] =
useState< { label: string; value: string }[] >( defaultPost );
const [ searchResults, setSearchResults ] = useState< SearchResult[] >(
[]
);
const [ isLoading, setIsLoading ] = useState( false );
const [ value, setValue ] = useState< string | null >(
data?.post?.toString() ?? null
);
const postTypes = useSelect(
( select ) => select( coreStore ).getPostTypes(),
[]
);
const handleDetach = () => {
onChange( {
post: 0,
_embedded: { ...data?._embedded, 'wp:attached-to': undefined },
} );
setValue( null );
setOptions( [] );
};
const onValueChange = async ( filterValue: string ) => {
setIsLoading( true );
const results = await fetchLinkSuggestions(
filterValue,
/*
* @TODO `fetchLinkSuggestions()` should accept `perPage` as an option argument.
* `isInitialSuggestions` limits the result to 3, otherwise it's hardcoded to 20.
*/
{ type: 'post', isInitialSuggestions: true },
{}
);
setSearchResults( results );
const suggestions = results.map( ( result ) => {
return {
label: result.title,
value: result.id.toString(),
};
} );
const includeCurrent =
! filterValue &&
suggestions.findIndex( ( s ) => s.value === value ) === -1;
setOptions( suggestions.concat( includeCurrent ? defaultPost : [] ) );
setIsLoading( false );
};
/**
* Handle selection.
*
* @param {Object} selectedPostId The selected post id.
*/
const handleSelectOption = (
selectedPostId: string | null | undefined
) => {
if ( ! selectedPostId ) {
handleDetach();
return;
}
setValue( selectedPostId );
if ( selectedPostId ) {
const selectedPost = searchResults.find(
( result ) => result.id === Number( selectedPostId )
);
// Although unlikely, it's technically possible for selectedPost to not be found.
// E.g. if the user selects an option just as new search results are loaded.
// TODO: Add error handling for when selectedPost is not found.
if ( selectedPost && postTypes ) {
const postType = postTypes.find(
( _postType: { slug: string } ) =>
_postType.slug === selectedPost?.type
);
const attachedTo = {
...( postType && { type: postType.slug } ),
id: Number( selectedPostId ),
title: {
raw: selectedPost.title,
rendered: selectedPost.title,
},
};
onChange( {
post: Number( selectedPostId ),
_embedded: {
...data?._embedded,
'wp:attached-to': [ attachedTo ],
},
} );
}
}
};
const help = createInterpolateElement(
__(
'Search for a post or page to attach this media to or .'
),
{
button: (
),
}
);
return (
onValueChange( filterValue as string ),
300
) }
onChange={ handleSelectOption }
hideLabelFromVision
/>
);
}