Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): fix reference preview flickering and improve loading #7563

Merged
merged 10 commits into from
Oct 8, 2024
38 changes: 6 additions & 32 deletions dev/test-studio/schema/debug/simpleReferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,17 @@ export const simpleReferences = {
title: 'Title',
type: 'string',
},
{
name: 'image',
title: 'Title',
type: 'image',
},
{
name: 'referenceField',
title: 'Reference field',
description: 'A simple reference field',
type: 'reference',
to: [{type: 'author'}],
},
{
name: 'arrayWithObjects',
options: {collapsible: true, collapsed: true},
title: 'Array with named objects',
description: 'This array contains objects of type as defined inline',
type: 'array',
of: [
{
type: 'object',
name: 'something',
title: 'Something',
// options: {modal: 'inline'},
fields: [
{name: 'first', type: 'string', title: 'First string'},
{name: 'second', type: 'string', title: 'Second string'},
],
},
{
type: 'object',
name: 'otherThing',
title: 'OtherThing',
options: {modal: 'inline'},
fields: [{name: 'value', type: 'string', title: 'First string'}],
},
{
type: 'reference',
title: 'A reference to an author or a book',
to: [{type: 'author'}, {type: 'book'}],
},
],
to: [{type: 'simpleReferences'}],
},
],
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const SubtitleSkeleton = styled(TextSkeleton).attrs({animated: true, radius: 1,
max-width: ${rem(120)};
width: 60%;
`

const SKELETON_DELAY = 300
/**
* @hidden
* @beta */
Expand All @@ -75,13 +75,18 @@ export function DefaultPreview(props: DefaultPreviewProps) {
<Flex align="center" flex={1} gap={2}>
{media && (
<Box flex="none">
<Skeleton animated radius={1} style={PREVIEW_SIZES.default.media} />
<Skeleton
animated
delay={SKELETON_DELAY}
radius={1}
style={PREVIEW_SIZES.default.media}
/>
</Box>
)}

<Stack data-testid="default-preview__heading" flex={1} space={2}>
<TitleSkeleton />
<SubtitleSkeleton />
<TitleSkeleton delay={SKELETON_DELAY} />
<SubtitleSkeleton delay={SKELETON_DELAY} />
</Stack>

<Box flex="none" padding={1}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const AutocompleteContainer = forwardRef(function AutocompleteContainer(
const inputWrapperRect = useElementRect(rootElement)

return (
<Root ref={handleNewRef} gap={1} $narrow={(inputWrapperRect?.width || 0) < 480}>
<Root ref={handleNewRef} gap={1} $narrow={(inputWrapperRect?.width || 480) < 480}>
{props.children}
</Root>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import {ReferenceFinalizeAlertStrip} from './ReferenceFinalizeAlertStrip'
import {ReferenceLinkCard} from './ReferenceLinkCard'
import {ReferenceMetadataLoadErrorAlertStrip} from './ReferenceMetadataLoadFailure'
import {ReferenceStrengthMismatchAlertStrip} from './ReferenceStrengthMismatchAlertStrip'
import {useReferenceInfo} from './useReferenceInfo'
import {type ReferenceInfo} from './types'
import {type Loadable, useReferenceInfo} from './useReferenceInfo'
import {useReferenceInput} from './useReferenceInput'

interface ReferenceFieldProps extends Omit<ObjectFieldProps, 'renderDefault'> {
Expand Down Expand Up @@ -88,7 +89,10 @@ export function ReferenceField(props: ReferenceFieldProps) {
const hasErrors = props.validation.some((v) => v.level === 'error')
const hasWarnings = props.validation.some((v) => v.level === 'warning')

const loadableReferenceInfo = useReferenceInfo(value?._ref, getReferenceInfo)
const loadableReferenceInfo: Loadable<ReferenceInfo> = useReferenceInfo(
pedrobonamin marked this conversation as resolved.
Show resolved Hide resolved
value?._ref,
getReferenceInfo,
)

const refTypeName = loadableReferenceInfo.result?.type || value?._strengthenOnPublish?.type

Expand Down
10 changes: 8 additions & 2 deletions packages/sanity/src/core/form/inputs/ReferenceInput/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ import {type Observable} from 'rxjs'
import {type DocumentAvailability} from '../../../preview'
import {type ObjectInputProps} from '../../types'

export type PreviewDocumentValue = PreviewValue & {
_id: string
_createdAt?: string
_updatedAt?: string
}

export interface ReferenceInfo {
id: string
type: string | undefined
availability: DocumentAvailability
preview: {
draft: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined
published: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined
draft: PreviewDocumentValue | undefined
published: PreviewDocumentValue | undefined
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useCallback, useMemo} from 'react'
import {observableCallback} from 'observable-callback'
import {useMemo} from 'react'
import {useObservable} from 'react-rx'
import {concat, type Observable, of, Subject} from 'rxjs'
import {concat, type Observable, of} from 'rxjs'
import {catchError, concatMap, map, startWith} from 'rxjs/operators'

import {type ReferenceInfo} from './types'
Expand Down Expand Up @@ -33,16 +34,11 @@ export function useReferenceInfo(
getReferenceInfo: GetReferenceInfo,
): Loadable<ReferenceInfo> {
// NOTE: this is a small message queue to handle retries
const msgSubject = useMemo(() => new Subject<{type: 'retry'}>(), [])
const msg$ = useMemo(() => msgSubject.asObservable(), [msgSubject])

const retry = useCallback(() => {
msgSubject.next({type: 'retry'})
}, [msgSubject])
const [onRetry$, onRetry] = useMemo(() => observableCallback(), [])

const referenceInfoObservable = useMemo(
() =>
concat(of(null), msg$).pipe(
concat(of(null), onRetry$).pipe(
map(() => id),
concatMap((refId: string | undefined) =>
refId
Expand All @@ -52,19 +48,24 @@ export function useReferenceInfo(
isLoading: false,
result,
error: undefined,
retry,
retry: onRetry,
} as const
}),
startWith(INITIAL_LOADING_STATE),
catchError((err: Error) => {
console.error(err)
return of({isLoading: false, result: undefined, error: err, retry} as const)
return of({
isLoading: false,
result: undefined,
error: err,
retry: onRetry,
} as const)
}),
)
: of(EMPTY_STATE),
),
),
[getReferenceInfo, id, msg$, retry],
[getReferenceInfo, id, onRetry, onRetry$],
)
return useObservable(referenceInfoObservable, INITIAL_LOADING_STATE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import {type SanityClient} from '@sanity/client'
import {DEFAULT_MAX_FIELD_DEPTH} from '@sanity/schema/_internal'
import {type ReferenceFilterSearchOptions, type ReferenceSchemaType} from '@sanity/types'
import {combineLatest, type Observable, of} from 'rxjs'
import {map, mergeMap, startWith, switchMap} from 'rxjs/operators'
import {map, mergeMap, switchMap} from 'rxjs/operators'

import {type DocumentPreviewStore, getPreviewPaths, prepareForPreview} from '../../../../preview'
import {type DocumentPreviewStore} from '../../../../preview'
import {createSearch} from '../../../../search'
import {collate, type CollatedHit, getDraftId, getIdPair, isRecord} from '../../../../util'
import {type ReferenceInfo, type ReferenceSearchHit} from '../../../inputs/ReferenceInput/types'
import {collate, type CollatedHit, getDraftId, getIdPair} from '../../../../util'
import {
type PreviewDocumentValue,
type ReferenceInfo,
type ReferenceSearchHit,
} from '../../../inputs/ReferenceInput/types'

const READABLE = {
available: true,
Expand Down Expand Up @@ -58,9 +62,6 @@ export function getReferenceInfo(
} as const)
}

const draftRef = {_type: 'reference', _ref: draftId}
const publishedRef = {_type: 'reference', _ref: publishedId}

const typeName$ = combineLatest([
documentPreviewStore.observeDocumentTypeFromId(draftId),
documentPreviewStore.observeDocumentTypeFromId(publishedId),
Expand Down Expand Up @@ -102,33 +103,15 @@ export function getReferenceInfo(
} as const)
}

const previewPaths = getPreviewPaths(refSchemaType?.preview) || []

const draftPreview$ = documentPreviewStore.observePaths(draftRef, previewPaths).pipe(
map((result) =>
result
? {
_id: draftId,
...prepareForPreview(result, refSchemaType),
}
: undefined,
),
startWith(undefined),
const draftPreview$ = documentPreviewStore.observeForPreview(
pedrobonamin marked this conversation as resolved.
Show resolved Hide resolved
{_id: draftId},
refSchemaType,
)

const publishedPreview$ = documentPreviewStore
.observePaths(publishedRef, previewPaths)
.pipe(
map((result) =>
result
? {
_id: publishedId,
...prepareForPreview(result, refSchemaType),
}
: undefined,
),
startWith(undefined),
)
const publishedPreview$ = documentPreviewStore.observeForPreview(
{_id: publishedId},
refSchemaType,
)

const value$ = combineLatest([draftPreview$, publishedPreview$]).pipe(
map(([draft, published]) => ({
Expand All @@ -152,8 +135,10 @@ export function getReferenceInfo(
id: publishedId,
availability,
preview: {
draft: isRecord(value.draft) ? value.draft : undefined,
published: isRecord(value.published) ? value.published : undefined,
draft: (value.draft.snapshot || undefined) as PreviewDocumentValue | undefined,
published: (value.published.snapshot || undefined) as
| PreviewDocumentValue
| undefined,
},
}
}),
Expand Down
9 changes: 8 additions & 1 deletion packages/sanity/src/core/preview/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {combineLatest, defer, from, type Observable, of} from 'rxjs'
import {distinctUntilChanged, map, mergeMap, reduce, switchMap} from 'rxjs/operators'
import shallowEquals from 'shallow-equals'

import {getDraftId, getPublishedId, isRecord} from '../util'
import {createSWR, getDraftId, getPublishedId, isRecord} from '../util'
import {
AVAILABILITY_NOT_FOUND,
AVAILABILITY_PERMISSION_DENIED,
Expand All @@ -22,6 +22,11 @@ import {debounceCollect} from './utils/debounceCollect'

const MAX_DOCUMENT_ID_CHUNK_SIZE = 11164

/**
* Create an SWR operator for document availability
*/
const swr = createSWR<DocumentAvailability>({maxSize: 1000})

/**
* Takes an array of document IDs and puts them into individual chunks.
* Because document IDs can vary greatly in size, we want to chunk by the length of the
Expand Down Expand Up @@ -89,6 +94,8 @@ export function createPreviewAvailabilityObserver(
: // we can't read the _rev field for two possible reasons: 1) the document isn't readable or 2) the document doesn't exist
fetchDocumentReadability(id)
}),
swr(id),
map((ev) => ev.value),
)
}

Expand Down
10 changes: 6 additions & 4 deletions packages/sanity/src/core/preview/components/PreviewLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ export function PreviewLoader(
const [element, setElement] = useState<HTMLDivElement | null>(null)

// Subscribe to visibility
const isVisible = useVisibility({
element: skipVisibilityCheck ? null : element,
hideDelay: _HIDE_DELAY,
})
const isVisible =
useVisibility({
disabled: skipVisibilityCheck,
element: element,
hideDelay: _HIDE_DELAY,
}) || skipVisibilityCheck

// Subscribe document preview value
const preview = useValuePreview({
Expand Down
10 changes: 8 additions & 2 deletions packages/sanity/src/core/preview/useValuePreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ interface State {
const INITIAL_STATE: State = {
isLoading: true,
}
const PENDING_STATE: State = {

const IDLE_STATE: State = {
isLoading: false,
value: {
title: undefined,
description: undefined,
},
}
/**
* @internal
Expand All @@ -33,7 +38,8 @@ function useDocumentPreview(props: {
const {enabled = true, ordering, schemaType, value: previewValue} = props || {}
const {observeForPreview} = useDocumentPreviewStore()
const observable = useMemo<Observable<State>>(() => {
if (!enabled || !previewValue || !schemaType) return of(PENDING_STATE)
// this will render previews as "loaded" (i.e. not in loading state) – typically with "Untitled" text
if (!enabled || !previewValue || !schemaType) return of(IDLE_STATE)

return observeForPreview(previewValue as Previewable, schemaType, {
viewOptions: {ordering: ordering},
Expand Down
29 changes: 22 additions & 7 deletions packages/sanity/src/core/preview/useVisibility.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import {useEffect, useState} from 'react'
import {useLayoutEffect, useState} from 'react'
import {concat, of} from 'rxjs'
import {delay, distinctUntilChanged, map, switchMap} from 'rxjs/operators'

import {intersectionObservableFor} from './streams/intersectionObservableFor'
import {visibilityChange$} from './streams/visibilityChange'

export function useVisibility(props: {element: HTMLElement | null; hideDelay?: number}): boolean {
const {element, hideDelay = 0} = props
interface Props {
/**
* Disable the check. The hook will return false if disabled
*/
disabled?: boolean
/** DOM Node to check visibility for */
element: HTMLElement | null
/** When element is hidden, wait this delay in milliseconds before reporting it as */
hideDelay?: number
}

export function useVisibility(props: Props): boolean {
const {element, hideDelay = 0, disabled} = props
const [visible, setVisible] = useState(false)

useEffect(() => {
if (!element) {
useLayoutEffect(() => {
if (!element || disabled) {
return undefined
}

if (element && 'checkVisibility' in element) {
setVisible(element.checkVisibility())
}

const isDocumentVisible$ = concat(
of(!document.hidden),
visibilityChange$.pipe(
Expand All @@ -34,7 +49,7 @@ export function useVisibility(props: {element: HTMLElement | null; hideDelay?: n
const sub = visible$.subscribe(setVisible)

return () => sub.unsubscribe()
}, [element, hideDelay])
}, [element, hideDelay, disabled])

return visible
return disabled ? false : visible
}
Loading
Loading