Created
December 10, 2021 11:03
-
-
Save SimeonGriggs/5e5f555576da2c880b98342c9ea4184d to your computer and use it in GitHub Desktop.
Reference Field Custom Input with Restrained Search
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
/* eslint-disable react/prop-types */ | |
/** | |
* 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 reference field | |
* It's also a bit wonky in terms of the UI, but it's a start | |
* ...and 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