Skip to content

Commit

Permalink
refactor(form-builder): migrate reference input to use Sanity UI
Browse files Browse the repository at this point in the history
Co-Authored-By: Marius Lundgård <studio@mariuslundgard.com
  • Loading branch information
bjoerge committed Apr 26, 2021
1 parent 89b0bc9 commit 43a192b
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 334 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './ReferenceInput'
export {default} from './ReferenceInput'

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// this conditionally enables strict mode for everything in this folder
// (Note: only applies to the typescript compiler service, not when using tsc for build)
{"extends": "../../../tsconfig.strict"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {useEffect, useState} from 'react'
import {Reference} from '@sanity/types'
import {tap} from 'rxjs/operators'
import {Observable} from 'rxjs'

type SnapshotState = {
isLoading: boolean
snapshot: null | PreviewSnapshot
}

const LOADING_SNAPSHOT: SnapshotState = {
isLoading: true,
snapshot: null,
}

const NULL_SNAPSHOT: SnapshotState = {
isLoading: false,
snapshot: null,
}

type PreviewSnapshot = {
_id: string
_type: string
title: string
description: string
}

export function usePreviewSnapshot(
value: Reference | undefined,
getPreviewSnapshot: (reference: Reference) => Observable<PreviewSnapshot | null>
): SnapshotState {
const [state, setState] = useState<SnapshotState>(LOADING_SNAPSHOT)

// eslint-disable-next-line consistent-return
useEffect(() => {
if (value?._ref) {
setState(LOADING_SNAPSHOT)
const sub = getPreviewSnapshot(value)
.pipe(tap((snapshot) => setState({isLoading: false, snapshot})))
.subscribe()
return () => {
sub.unsubscribe()
}
}
setState(NULL_SNAPSHOT)
}, [getPreviewSnapshot, value])
return state
}
129 changes: 111 additions & 18 deletions packages/@sanity/form-builder/src/sanity/inputs/SanityReferenceInput.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,117 @@
import React from 'react'
import React, {ForwardedRef, forwardRef, useCallback, useRef} from 'react'

import {search, getPreviewSnapshot} from './client-adapters/reference'
import ReferenceInput, {Props} from '../../inputs/ReferenceInput'
import {
Marker,
Path,
Reference,
ReferenceFilterSearchOptions,
ReferenceOptions,
ReferenceSchemaType,
SanityDocument,
} from '@sanity/types'
import {get} from '@sanity/util/paths'
import {FormFieldPresence} from '@sanity/base/lib/presence'
import {from, throwError} from 'rxjs'
import {catchError, mergeMap} from 'rxjs/operators'
import {ReferenceInput} from '../../inputs/ReferenceInput'
import PatchEvent from '../../PatchEvent'
import withValuePath from '../../utils/withValuePath'
import withDocument from '../../utils/withDocument'
import * as adapter from './client-adapters/reference'

export default class SanityReference extends React.Component<Props> {
_input: any
setInput = (input) => {
this._input = input
// eslint-disable-next-line require-await
async function resolveUserDefinedFilter(
options: ReferenceOptions | undefined,
document: SanityDocument,
valuePath: Path
): Promise<ReferenceFilterSearchOptions> {
if (!options) {
return {}
}
focus() {
this._input.focus()

if (typeof options.filter === 'function') {
const parentPath = valuePath.slice(0, -1)
const parent = get(document, parentPath) as Record<string, unknown>
return options.filter({document, parentPath, parent})
}
render() {
return (
<ReferenceInput
{...this.props}
onSearch={search}
getPreviewSnapshot={getPreviewSnapshot}
ref={this.setInput}
/>
)

return {
filter: options.filter,
params: 'filterParams' in options ? options.filterParams : undefined,
}
}

export type Props = {
value?: Reference
compareValue?: Reference
type: ReferenceSchemaType
markers: Marker[]
focusPath: Path
readOnly?: boolean
onFocus: (path: Path) => void
onChange: (event: PatchEvent) => void
level: number
presence: FormFieldPresence[]

// From withDocument
document: SanityDocument

// From withValuePath
getValuePath: () => Path
}

function useValueRef<T>(value: T): {current: T} {
const ref = useRef(value)
ref.current = value
return ref
}

type SearchError = {
message: string
details?: {
type: string
description: string
}
}

const SanityReferenceInput = forwardRef(function SanityReferenceInput(
props: Props,
ref: ForwardedRef<HTMLInputElement>
) {
const {getValuePath, type, document} = props

const documentRef = useValueRef(document)

const handleSearch = useCallback(
(searchString: string) =>
from(resolveUserDefinedFilter(type.options, documentRef.current, getValuePath())).pipe(
mergeMap(({filter, params}) =>
adapter.search(searchString, type, {...type.options, filter, params})
),
catchError((err: SearchError) => {
const isQueryError = err.details && err.details.type === 'queryParseError'
if (type.options?.filter && isQueryError) {
err.message = `Invalid reference filter, please check the custom "filter" option`
}
return throwError(err)
})
),
[documentRef, getValuePath, type]
)

const getPreviewSnapshot = useCallback(
(value: Reference) => adapter.getPreviewSnapshot(value, type),
[type]
)

return (
<ReferenceInput
{...props}
onSearch={handleSearch}
getPreviewSnapshot={getPreviewSnapshot}
ref={ref}
/>
)
})

export default withValuePath(withDocument(SanityReferenceInput))
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import {map} from 'rxjs/operators'
import {ReferenceFilterSearchOptions, ReferenceSchemaType} from '@sanity/types'
import {Observable} from 'rxjs'
import {createWeightedSearch, observeForPreview} from '../../../legacyParts'
import {versionedClient} from '../../versionedClient'

export function getPreviewSnapshot(value, referenceType) {
export function getPreviewSnapshot(value: {_ref: string}, referenceType: ReferenceSchemaType) {
return observeForPreview(value, referenceType).pipe(map((result: any) => result.snapshot))
}

export function search(textTerm, referenceType, options) {
const doSearch = createWeightedSearch(referenceType.to, versionedClient, options)
return doSearch(textTerm, {includeDrafts: false}).pipe(
map((results: any[]) => results.map((res) => res.hit))
type SearchHit = {
_id: string
_type: string
}

type SearchResult = {hit: SearchHit}[]

export function search(
textTerm: string,
type: ReferenceSchemaType,
options: ReferenceFilterSearchOptions
): Observable<SearchHit[]> {
const searchWeighted = createWeightedSearch(type.to, versionedClient, options)
return searchWeighted(textTerm, {includeDrafts: false}).pipe(
map((results: SearchResult): SearchHit[] =>
results.map(({hit}) => ({_type: hit._type, _id: hit._id}))
)
)
}
3 changes: 3 additions & 0 deletions packages/@sanity/form-builder/src/sanity/inputs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// this conditionally enables strict mode for everything in this folder
// (Note: only applies to the typescript compiler service, not when using tsc for build)
{"extends": "../../../tsconfig.strict"}
42 changes: 42 additions & 0 deletions packages/@sanity/form-builder/src/utils/useObservableCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, {DependencyList, useCallback} from 'react'
import {observableCallback} from 'observable-callback'
import {Observable} from 'rxjs'

export type AsyncCompleteState<T> = {
status: 'complete'
result: T
}
export type AsyncPendingState = {
status: 'pending'
}
export type AsyncErrorState = {
status: 'error'
error: Error
}

export type AsyncState<T> = AsyncPendingState | AsyncCompleteState<T> | AsyncErrorState

const EMPTY_DEPS: DependencyList = []

export function useObservableCallback<T, U>(
fn: (arg: Observable<T>) => Observable<U>,
dependencies: DependencyList = EMPTY_DEPS
): (arg: T) => void {
const callbackRef = React.useRef(null)
if (callbackRef.current === null) {
callbackRef.current = observableCallback<U>()
}
const [calls$, call] = callbackRef.current

// eslint-disable-next-line react-hooks/exhaustive-deps
const callback = useCallback(fn, dependencies)

React.useEffect(() => {
const subscription = calls$.pipe(callback).subscribe()
return () => {
subscription.unsubscribe()
}
}, [calls$, call, callback])

return call
}

0 comments on commit 43a192b

Please sign in to comment.