Last active
September 13, 2021 22:01
-
-
Save runeb/1a2fac7b9bd6e00c6a51375f4adf5884 to your computer and use it in GitHub Desktop.
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
export default { | |
type: 'document', | |
name: 'article', | |
title: 'Article', | |
fields: [ | |
{ | |
type: 'string', | |
name: 'title' | |
}, | |
{ | |
name: 'productRef', | |
title: 'Product', | |
type: 'externalReference', | |
options: { | |
to: [ | |
{ | |
type: 'product', | |
// We need to specify how to preview this doocument, since its not part of the current schema | |
preview: { | |
select: { | |
title: 'title' | |
} | |
} | |
} | |
], | |
// The fields we search with when editor wants to make a reference | |
searchFields: ['title'], | |
// The name of the dataset it resides in | |
dataset: 'other', | |
}, | |
} | |
] | |
} |
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
import React, { useRef } from 'react' | |
import { map } from 'rxjs/operators' | |
import { createWeightedSearch } from 'part:@sanity/base/search/weighted' | |
import client from 'part:@sanity/base/client' | |
import ExternalReferenceInput from './ExternalReferenceInput' | |
const ExternalReference = props => { | |
const { type } = props | |
const { options } = type | |
const { dataset } = options | |
// Configure a client pointed at the other dataset | |
const externalClient = client.clone().config({ dataset }) | |
// Figure out how to search for the documents in | |
// the other dataset. Use the options.searchFields array | |
const searchFields = (options.searchFields || []) | |
const searchTerms = searchFields.map(field => ( | |
{ | |
weight: 1 / searchFields.length, | |
path: [field] | |
} | |
)) | |
// This is how we search for documents in the other dataset | |
const search = (textTerm) => { | |
const doSearch = createWeightedSearch(options.to.map(to => ({ | |
name: to.type, | |
__experimental_search: searchTerms | |
})), externalClient) | |
return doSearch(textTerm, { includeDrafts: false }).pipe( | |
map((results) => results.map(res => res.hit)) | |
) | |
} | |
return ( | |
<ExternalReferenceInput | |
{...props} | |
onSearch={search} | |
client={externalClient} | |
/> | |
) | |
} | |
export default ExternalReference |
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
@import "part:@sanity/base/theme/variables-style"; | |
.hasWarnings { | |
border: 1px dashed var(--state-warning-color); | |
} | |
.hasWarnings input { | |
color: var(--legend-color); | |
} |
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
import React, { useState, useEffect, useRef } from 'react' | |
import SearchableSelect from 'part:@sanity/components/selects/searchable' | |
import FormField from 'part:@sanity/components/formfields/default' | |
//import Preview from '@sanity/form-builder/lib/Preview' | |
import subscriptionManager from '@sanity/form-builder/lib/utils/subscriptionManager' | |
import { uniqueId } from 'lodash' | |
import { FOCUS_TERMINATOR } from '@sanity/util/paths' | |
import PatchEvent, { set, unset, setIfMissing } from '@sanity/form-builder/PatchEvent' | |
import styles from './ExternalReferenceInput.css' | |
const ExternalReferenceInput = props => { | |
const [hits, setHits] = useState([]) | |
const [previewSnapshot, setPreviewSnapshot] = useState(null) | |
const [isMissing, setIsMissing] = useState(false) | |
const [isFetching, setIsFetching] = useState(false) | |
const [subscriptions] = useState(() => { | |
return subscriptionManager('search', 'previewSnapshot') | |
}) | |
const input = useRef(null) | |
const _inputId = uniqueId('ExternalReferenceInput') | |
const { type, value, level, markers, readOnly, presence } = props | |
useEffect(() => { | |
if (subscriptions) { | |
getPreviewSnapshot(value) | |
} | |
}, [subscriptions, value]) | |
useEffect(() => { | |
return () => { | |
if (subscriptions) { | |
subscriptions.unsubscribeAll() | |
} | |
} | |
}, []) | |
useEffect(() => { | |
setHits([]) | |
setPreviewSnapshot(null) | |
setIsMissing(false) | |
setIsFetching(false) | |
}, [value]) | |
const getPreviewSnapshot = (value) => { | |
if (!value || !value.id) { return } | |
const { client } = props | |
const query = '* [_id == $id][0]' | |
const params = { id: value.id } | |
client.fetch(query, params).then(doc => { | |
setPreviewSnapshot(getPreviewSnapshotValues(doc)) | |
setIsMissing(!doc) | |
}) | |
subscriptions.replace( | |
'previewSnapshot', | |
client.listen('* [_id == $id]', { id: value.id }).subscribe(update => { | |
setPreviewSnapshot(getPreviewSnapshotValues(update.result)) | |
setIsMissing(!update.result) | |
}) | |
) | |
} | |
const getPreviewSnapshotValues = (doc) => { | |
if (!doc) return null | |
const refType = getMemberTypeFor(doc._type) | |
const { preview } = refType | |
return { | |
title: doc[preview.select.title] | |
} | |
} | |
const getMemberTypeFor = (typeName) => { | |
const { type } = props | |
const { options } = type | |
return options.to.find(ofType => ofType.type === typeName) | |
} | |
const handleFocus = () => { | |
const { onFocus } = props | |
if (onFocus) { | |
onFocus([FOCUS_TERMINATOR]) | |
} | |
} | |
const handleChange = (item) => { | |
const { type, onChange } = props | |
const { options } = type | |
onChange( | |
PatchEvent.from( | |
setIfMissing({ | |
_type: type.name, | |
dataset: options.dataset, | |
id: item._id | |
}), | |
set(item._id, ['id']), | |
set(options.dataset, ['dataset']) | |
) | |
) | |
} | |
const handleClear = () => { | |
props.onChange(PatchEvent.from(unset())) | |
} | |
const handleSearch = query => search(query) | |
const handleOpen = () => search('') | |
const resolveUserDefinedFilter = () => { | |
const { type, document, getValuePath } = props | |
const options = type.options | |
if (!options) { return {} } | |
const { filter, filterParams: params } = options | |
if (typeof filter === 'function') { | |
const parentPath = getValuePath().slice(0, -1) | |
const parent = get(document, parentPath) | |
return filter({ document, parentPath, parent }) | |
} | |
return { filter, params } | |
} | |
const search = (query) => { | |
const { type, onSearch } = props | |
const options = resolveUserDefinedFilter() | |
setIsFetching(true) | |
subscriptions.replace( | |
'search', | |
onSearch(query, type, options).subscribe({ | |
next: (items) => { | |
setHits(items) | |
setIsFetching(false) | |
}, | |
error: (err) => { | |
const isQueryError = err.details && err.details.type === 'queryParseError' | |
if (!isQueryError || !resolveUserDefinedFilter().filter) { | |
throw err | |
} | |
err.message = 'Invalid reference filter, please check `filter`!' | |
throw err | |
} | |
}) | |
) | |
} | |
const renderHit = (item) => { | |
// TODO: Need to fix this for nice previews in search list. Also stop using the first weight (w0) as the property here | |
return <span>{item.w0}</span> | |
} | |
const focus = () => { | |
if (input.current) { | |
input.current.focus() | |
} | |
} | |
const valueFromHit = value && hits.find(hit => hit._id === value.id) | |
const hasRef = value && value.id | |
const validation = markers.filter(marker => marker.type === 'validation') | |
const errors = validation.filter(marker => marker.level === 'error') | |
let inputValue = value ? previewSnapshot && previewSnapshot.title : undefined | |
if (previewSnapshot && !previewSnapshot.title) { | |
inputValue = 'Untitled document' | |
} | |
const isLoadingSnapshot = value && value.id && !previewSnapshot | |
const placeholder = isLoadingSnapshot ? 'Loading…' : 'Type to search…' | |
return ( | |
<FormField | |
labelFor={_inputId} | |
markers={markers} | |
label={type.title} | |
level={level} | |
description={type.description} | |
presence={presence} | |
> | |
<div className={isMissing ? styles.hasWarnings : ''}> | |
<SearchableSelect | |
inputId={_inputId} | |
placeholder={readOnly ? '' : placeholder} | |
title={ | |
isMissing && hasRef | |
? `Referencing nonexistent document (id: ${value.id || 'unknown'})` | |
: previewSnapshot && previewSnapshot.description | |
} | |
customValidity={errors.length > 0 ? errors[0].item.message : ''} | |
onOpen={handleOpen} | |
onFocus={handleFocus} | |
onSearch={handleSearch} | |
onChange={handleChange} | |
onClear={handleClear} | |
//openItemElement={this.renderOpenItemElement} | |
value={valueFromHit || value} | |
inputValue={isMissing ? '<inaccessible or nonexistent reference>' : inputValue} | |
renderItem={renderHit} | |
isLoading={isFetching || isLoadingSnapshot} | |
items={hits} | |
ref={input} | |
readOnly={readOnly || isLoadingSnapshot} | |
/> | |
</div> | |
</FormField> | |
) | |
} | |
export default ExternalReferenceInput |
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
import ExternalReference from "../../src/inputComponents/ExternalReference"; | |
export default { | |
type: 'object', | |
name: 'externalReference', | |
fields: [ | |
{ | |
type: 'string', | |
name: 'dataset' | |
}, | |
// Note we do not use '_ref' here, since that is a special name on Sanity. | |
{ | |
type: 'string', | |
name: 'id' | |
} | |
], | |
inputComponent: ExternalReference | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment