Skip to content

Instantly share code, notes, and snippets.

@SimeonGriggs
Last active June 16, 2022 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SimeonGriggs/4ab143743521ed440951e3a64742762a to your computer and use it in GitHub Desktop.
Save SimeonGriggs/4ab143743521ed440951e3a64742762a to your computer and use it in GitHub Desktop.
An unofficial custom input for the Reference field with specific search paths + a custom document preview
import React from 'react'
import {Text, Spinner, Card} from '@sanity/ui'
import Preview from 'part:@sanity/base/preview'
import schema from 'part:@sanity/base/schema'
import useListeningQuery from './useListeningQuery'
export default function CustomPreview(props) {
const {id} = props
const query = `*[_id == $id][0]`
const {data, loading, error} = useListeningQuery(query, {id})
if (loading) {
return <Spinner />
}
if (error || !data) {
return (
<Card tone="critical" padding={2} shadow={1}>
<Text>Error: Could not load preview</Text>
</Card>
)
}
const schemaOriginal = schema.get(data._type)
const schemaCustom = {
...schemaOriginal,
preview: {
select: {
title: 'title',
slug: 'slug.current',
media: 'image',
},
prepare({title, slug, media}) {
return {
title,
subtitle: slug,
media,
}
},
},
}
return (
<Card shadow={1} padding={2}>
<Preview value={data} type={schemaCustom} />
</Card>
)
}
{
name: 'referenceFieldDisplay',
type: 'reference',
to: [{type: 'filterable'}],
inputComponent: ReferenceFieldValueDisplayer,
options: {
schemaTypes: ['filterable'],
searchPath: 'slug.current',
},
},
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react/display-name */
/* eslint-disable camelcase */
import React, {useCallback} from 'react'
import {Stack, Text, Autocomplete, Spinner, Button, Card} from '@sanity/ui'
import {SearchIcon} from '@sanity/icons'
import PatchEvent, {set, unset} from '@sanity/form-builder/PatchEvent'
import Preview from 'part:@sanity/base/preview'
import schema from 'part:@sanity/base/schema'
import useListeningQuery from '../../hooks/useListeningQuery'
import CustomPreview from './CustomPreview'
export const ReferenceFieldValueDisplayer = React.forwardRef((props, ref) => {
const {type, onChange, value} = props
const {searchPath} = type.options
const schemaTypes = type.to.map(({name}) => name)
const query = `*[_type in $schemaTypes]{
_id,
_type,
"injectValue": ${searchPath}
}`
const {data, loading, error} = useListeningQuery(query, {schemaTypes})
const options = data?.length
? data
.filter((doc) => Boolean(doc.injectValue))
.map((doc) => ({
// Need to use _id as value as <Autocomplete> uses it as a key
value: doc._id,
payload: doc,
}))
: []
const handleChange = useCallback(
(id) => {
if (!id) {
return onChange(PatchEvent.from(unset()))
}
const newValue = {
_type: `reference`,
_ref: id,
}
return onChange(PatchEvent.from(set(newValue)))
},
[options]
)
if (loading) {
return <Spinner />
}
if (error) {
return (
<Card tone="critical" padding={2} shadow={1}>
<Text>Error: Could not load documents</Text>
</Card>
)
}
const customSchemas = schemaTypes.reduce((acc, cur) => {
const original = schema.get(cur)
const custom = {
...original,
preview: {
select: {
title: 'title',
slug: 'slug.current',
media: 'image',
},
prepare({title, slug, media}) {
return {
title,
subtitle: slug,
media,
}
},
},
}
return {
...acc,
[cur]: custom,
}
}, {})
return (
<Stack space={3}>
<Stack space={2}>
<Text size={1} weight="medium">
{props?.type?.title}
</Text>
{props?.type?.description ? (
<Text size={1} muted>
{props?.type?.description}
</Text>
) : null}
</Stack>
<Autocomplete
icon={SearchIcon}
id="autocomplete-example"
options={options}
openButton
filterOption={(searchQuery, option) =>
String(option.payload.injectValue).toLowerCase().indexOf(searchQuery.toLowerCase()) > -1
}
renderValue={(value, option) => option?.payload.value || value}
renderOption={(option) => (
<Stack>
<Button mode="bleed" padding={2}>
<Preview value={option.payload} type={customSchemas[option.payload._type]} />
</Button>
</Stack>
)}
placeholder={`Search "${searchPath}" field in ${schemaTypes
.map((type) => `"${type}"`)
.join(`, `)} documents`}
onChange={handleChange}
/>
{value?._ref ? <CustomPreview id={value._ref} /> : null}
</Stack>
)
})
export default ReferenceFieldValueDisplayer
import React, {useEffect, useState, useRef} from 'react'
import documentStore from 'part:@sanity/base/datastore/document'
import {catchError, distinctUntilChanged} from 'rxjs/operators'
import isEqual from 'react-fast-compare'
type Params = Record<string, string | number | boolean | string[]>
interface ListenQueryOptions {
tag?: string
apiVersion?: string
}
type ReturnShape = {
loading: boolean
error: boolean
data: any
}
type Observable = {
unsubscribe: () => void
}
const DEFAULT_PARAMS = {}
const DEFAULT_OPTIONS = {apiVersion: `v2022-05-09`}
export default function useListeningQuery(
query: string,
params: Params = DEFAULT_PARAMS,
options: ListenQueryOptions = DEFAULT_OPTIONS
): ReturnShape {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const [data, setData] = useState(null)
const subscription = useRef<null | Observable>(null)
useEffect(() => {
if (query) {
subscription.current = documentStore
.listenQuery(query, params, options)
.pipe(
distinctUntilChanged(isEqual),
catchError((err) => {
console.error(err)
setError(err)
setLoading(false)
setData(null)
return err
})
)
.subscribe((documents) => {
setData((current) => (isEqual(current, documents) ? current : documents))
setLoading(false)
setError(false)
})
}
return () => {
return subscription.current ? subscription.current.unsubscribe() : undefined
}
}, [query, params, options])
return {loading, error, data}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment