Created
June 7, 2022 11:21
-
-
Save SimeonGriggs/d4c7822492778c93f5a87517c7cf7c15 to your computer and use it in GitHub Desktop.
Custom Reference Search Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This is a Custom Input POC for a Reference field with defined search parameters on the target document | |
* The built-in reference field currently can filter results but only statically, not using the search query | |
* | |
* This custom input is NOT recommended as it lacks some of the amazing features of the built-in refernce field | |
* It's also a bit wonky in terms of the UI, but it's a start | |
* But it does solve this one specific use case | |
*/ | |
import React from 'react' | |
import {FormField} from '@sanity/base/components' | |
import {Text, Card, Autocomplete} from '@sanity/ui' | |
import {SearchIcon} from '@sanity/icons' | |
import PatchEvent, {set, unset} from '@sanity/form-builder/PatchEvent' | |
import sanityClient from 'part:@sanity/base/client' | |
import schema from 'part:@sanity/base/schema' | |
import Preview from 'part:@sanity/base/preview' | |
const client = sanityClient.withConfig({apiVersion: `2021-05-19`}) | |
// Custom input for a field that would be initialised like this: | |
// { | |
// name: 'link', | |
// title: 'Link', | |
// description: | |
// 'Uses a custom input to search only specific field paths in the given document types', | |
// type: 'reference', | |
// to: [{type: 'category'}], | |
// inputComponent: CustomReferenceSearch, | |
// options: {searchPath: 'title'}, | |
// }, | |
async function searchDocs(searchTypes, searchPath, searchValue) { | |
// searchPath can't be a variable in the GROQ Query | |
const docs = await client.fetch( | |
`*[!(_id in path("drafts.**")) && _type in $searchTypes && ${searchPath} match $searchValue][0..50]`, | |
{ | |
searchTypes, | |
searchValue, | |
} | |
) | |
// console.log(docs) | |
return docs | |
} | |
function createAutocompleteOptions(results, searchPath) { | |
return results.map((result) => ({ | |
value: result._id, | |
doc: result, | |
payload: result[searchPath], | |
})) | |
} | |
const CustomReferenceSearch = React.forwardRef((props, ref) => { | |
const [results, setResults] = React.useState([]) | |
const { | |
type, // Schema information | |
value, // Current field value | |
readOnly, // Boolean if field is not editable | |
placeholder, // Placeholder text from the schema | |
markers, // Markers including validation rules | |
presence, // Presence information for collaborative avatars | |
compareValue, // Value to check for "edited" functionality | |
onFocus, // Method to handle focus state | |
onBlur, // Method to handle blur state | |
onChange, // Method to handle patch events | |
} = props | |
// Default: Reference field schema always has a `to` key | |
const searchTypes = React.useMemo(() => type.to.map((schemaType) => schemaType.name), [type]) | |
// Custom: determine which path in the doc to search for results | |
const searchPath = React.useMemo(() => type.options.searchPath, [type]) | |
// Creates a change handler for patching data | |
const handleQueryChange = React.useCallback( | |
async (searchQuery) => { | |
// console.log({searchQuery}) | |
let searchResults | |
// Async callback to search for results | |
if (searchQuery.length > 2) { | |
searchResults = await new Promise((resolve) => { | |
// console.log(`searchin...`) | |
resolve(searchDocs(searchTypes, searchPath, searchQuery)) | |
}) | |
if (searchResults.length) { | |
setResults(searchResults) | |
} | |
} | |
}, | |
[onChange] | |
) | |
const handleClick = React.useCallback((selectedDoc) => { | |
const newReference = { | |
_type: 'reference', | |
_ref: selectedDoc._id, | |
} | |
// console.log(`Setting new reference: ${JSON.stringify(newReference)}`) | |
onChange(PatchEvent.from(selectedDoc ? set(newReference) : unset())) | |
}, []) | |
const handleChange = React.useCallback((change) => { | |
// console.log(`clicked remove`, {change}) | |
if (!change) { | |
onChange(PatchEvent.from(unset())) | |
} | |
}, []) | |
if (!searchPath) { | |
return ( | |
<Card> | |
<Text> | |
Schema has no <code>options.searchPath</code> key | |
</Text> | |
</Card> | |
) | |
} | |
return ( | |
<FormField | |
description={type.description} // Creates description from schema | |
title={type.title} // Creates label from schema title | |
__unstable_markers={markers} // Handles all markers including validation | |
__unstable_presence={presence} // Handles presence avatars | |
compareValue={compareValue} // Handles "edited" status | |
> | |
<Card> | |
<Autocomplete | |
fontSize={[2]} | |
icon={SearchIcon} | |
openButton | |
onQueryChange={handleQueryChange} | |
onChange={handleChange} | |
options={createAutocompleteOptions(results)} | |
padding={[3]} | |
placeholder={ | |
placeholder ?? `Search the "${searchPath}" key on ${searchTypes.join(', ')} documents` | |
} | |
readOnly={readOnly} | |
renderOption={({doc}) => ( | |
<Card as="button" padding={1} onClick={() => handleClick(doc)}> | |
<Preview value={doc} type={schema.get(doc._type)} /> | |
</Card> | |
)} | |
value={value} | |
renderValue={(value) => value._ref} | |
onFocus={onFocus} | |
onBlur={onBlur} | |
ref={ref} | |
/> | |
</Card> | |
</FormField> | |
) | |
}) | |
CustomReferenceSearch.displayName = 'CustomReferenceSearch' | |
// Create the default export to import into our schema | |
export default CustomReferenceSearch |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment