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

feat(core): document store loader - swr in edit state #7552

Merged
merged 10 commits into from
Oct 9, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type SanityDocument, type Schema} from '@sanity/types'
import {combineLatest, type Observable} from 'rxjs'
import {map, publishReplay, refCount, startWith, switchMap} from 'rxjs/operators'

import {createSWR} from '../../../../util/rxSwr'
import {type PairListenerOptions} from '../getPairListener'
import {type IdPair, type PendingMutationsEvent} from '../types'
import {memoize} from '../utils/createMemoizer'
Expand All @@ -14,6 +15,8 @@ interface TransactionSyncLockState {
enabled: boolean
}

const swr = createSWR<[SanityDocument, SanityDocument, TransactionSyncLockState]>({maxSize: 50})

/**
* @hidden
* @beta */
Expand Down Expand Up @@ -42,6 +45,7 @@ export const editState = memoize(
typeName: string,
): Observable<EditStateFor> => {
const liveEdit = isLiveEditEnabled(ctx.schema, typeName)

return snapshotPair(
ctx.client,
idPair,
Expand All @@ -59,14 +63,15 @@ export const editState = memoize(
),
]),
),
map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({
swr(`${idPair.publishedId}-${idPair.draftId}`),
map(({value: [draftSnapshot, publishedSnapshot, transactionSyncLock], fromCache}) => ({
id: idPair.publishedId,
type: typeName,
draft: draftSnapshot,
published: publishedSnapshot,
liveEdit,
ready: true,
transactionSyncLock,
ready: !fromCache,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to pass fromCache as a separate property on EditState? The granularity might be useful in certain cases (can do this later if the need arises though)

transactionSyncLock: fromCache ? null : transactionSyncLock,
})),
startWith({
id: idPair.publishedId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
} from './document-pair/operationEvents'
import {type OperationsAPI} from './document-pair/operations'
import {validation} from './document-pair/validation'
import {getVisitedDocuments} from './getVisitedDocuments'
import {type PairListenerOptions} from './getPairListener'
import {createDocumentsStorage} from './documentsStorage'
import {getInitialValueStream, type InitialValueMsg, type InitialValueOptions} from './initialValue'
import {listenQuery, type ListenQueryOptions} from './listenQuery'
import {resolveTypeForDocument} from './resolveTypeForDocument'
Expand Down Expand Up @@ -162,7 +164,10 @@ export function createDocumentStore({
return editOperations(ctx, getIdPairFromPublished(publishedId), type)
},
editState(publishedId, type) {
return editState(ctx, getIdPairFromPublished(publishedId), type)
const idPair = getIdPairFromPublished(publishedId)

const edit = editState(ctx, idPair, type)
return edit
},
operationEvents(publishedId, type) {
return operationEvents({
Expand All @@ -185,7 +190,8 @@ export function createDocumentStore({
)
},
validation(publishedId, type) {
return validation(ctx, getIdPairFromPublished(publishedId), type)
const idPair = getIdPairFromPublished(publishedId)
return validation(ctx, idPair, type)
},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
})
}
// React to changes in hasRev only
// eslint-disable-next-line react-hooks/exhaustive-deps

Check warning on line 119 in packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
}, [hasRev])

const [formRef, setFormRef] = useState<null | HTMLDivElement>(null)
Expand Down Expand Up @@ -166,7 +166,7 @@
>
<PresenceOverlay margins={margins}>
<Box as="form" onSubmit={preventDefault} ref={setRef}>
{connectionState === 'connecting' || !ready ? (
{connectionState === 'connecting' && !editState?.draft && !editState?.published ? (
<Delay ms={300}>
{/* TODO: replace with loading block */}
<Flex align="center" direction="column" height="fill" justify="center">
Expand Down Expand Up @@ -205,7 +205,9 @@
onSetPathCollapsed={onSetCollapsedPath}
openPath={openPath}
presence={presence}
readOnly={connectionState === 'reconnecting' || formState.readOnly}
readOnly={
connectionState === 'reconnecting' || formState.readOnly || !editState?.ready
}
schemaType={formState.schemaType}
validation={validation}
value={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ function DocumentHeaderTab(props: {
viewId: string | null
}) {
const {icon, id, isActive, label, tabPanelId, viewId, ...rest} = props
const {ready} = useDocumentPane()
const {ready, editState} = useDocumentPane()
const {setView} = usePaneRouter()
const handleClick = useCallback(() => setView(viewId), [setView, viewId])

return (
<Tab
{...rest} // required to enable <TabList> keyboard navigation
aria-controls={tabPanelId}
disabled={!ready}
disabled={!ready && !editState?.draft && !editState?.published}
icon={icon}
id={id}
label={label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('DocumentHeaderTitle', () => {
const defaultProps = {
connectionState: 'connected',
schemaType: {title: 'Test Schema', name: 'testSchema'},
value: {title: 'Test Value'},
editState: {draft: {title: 'Test Value'}},
}

const defaultValue = {
Expand All @@ -63,16 +63,33 @@ describe('DocumentHeaderTitle', () => {
await waitFor(() => expect(getByText('Untitled')).toBeInTheDocument())
})

it('should return an empty fragment when connectionState is not "connected"', async () => {
it('should return an empty fragment when connectionState is not "connected" and editState is empty', async () => {
mockUseDocumentPane.mockReturnValue({
...defaultProps,
connectionState: 'connecting',
editState: null,
} as unknown as DocumentPaneContextValue)

const {container} = render(<DocumentHeaderTitle />)
await waitFor(() => expect(container.firstChild).toBeNull())
})

it('should render the header title when connectionState is not "connected" and editState has values', async () => {
mockUseDocumentPane.mockReturnValue({
...defaultProps,
connectionState: 'connecting',
} as unknown as DocumentPaneContextValue)

mockUseValuePreview.mockReturnValue({
...defaultValue,
error: undefined,
value: {title: 'Test Value'},
})

const {getByText} = render(<DocumentHeaderTitle />)
await waitFor(() => expect(getByText('Test Value')).toBeInTheDocument())
})

it('should return the title if it is provided', async () => {
mockUseDocumentPane.mockReturnValue({
...defaultProps,
Expand All @@ -89,7 +106,7 @@ describe('DocumentHeaderTitle', () => {
it('should return "New {schemaType?.title || schemaType?.name}" if documentValue is not provided', async () => {
mockUseDocumentPane.mockReturnValue({
...defaultProps,
value: null,
editState: null,
} as unknown as DocumentPaneContextValue)

const client = createMockSanityClient()
Expand Down Expand Up @@ -146,7 +163,7 @@ describe('DocumentHeaderTitle', () => {
expect(mockUseValuePreview).toHaveBeenCalledWith({
enabled: true,
schemaType: defaultProps.schemaType,
value: defaultProps.value,
value: defaultProps.editState.draft,
}),
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {structureLocaleNamespace} from '../../../../i18n'
import {useDocumentPane} from '../../useDocumentPane'

export function DocumentHeaderTitle(): ReactElement {
const {connectionState, schemaType, title, value: documentValue} = useDocumentPane()
const subscribed = Boolean(documentValue) && connectionState !== 'connecting'
const {connectionState, schemaType, title, editState} = useDocumentPane()
const documentValue = editState?.draft || editState?.published
const subscribed = Boolean(documentValue)

const {error, value} = useValuePreview({
enabled: subscribed,
Expand All @@ -15,7 +16,7 @@ export function DocumentHeaderTitle(): ReactElement {
})
const {t} = useTranslation(structureLocaleNamespace)

if (connectionState === 'connecting') {
if (connectionState === 'connecting' && !subscribed) {
return <></>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const DocumentPanelHeader = memo(
<PaneHeader
border
ref={ref}
loading={connectionState === 'connecting'}
loading={connectionState === 'connecting' && !editState?.draft && !editState?.published}
title={<DocumentHeaderTitle />}
tabs={showTabs && <DocumentHeaderTabs />}
tabIndex={tabIndex}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ interface UseDocumentTitle {
* @returns The document title or error. See {@link UseDocumentTitle}
*/
export function useDocumentTitle(): UseDocumentTitle {
const {connectionState, schemaType, title, value: documentValue} = useDocumentPane()
const subscribed = Boolean(documentValue) && connectionState !== 'connecting'
const {connectionState, schemaType, title, editState} = useDocumentPane()
const documentValue = editState?.draft || editState?.published
const subscribed = Boolean(documentValue)

const {error, value} = useValuePreview({
enabled: subscribed,
schemaType,
value: documentValue,
})

if (connectionState === 'connecting') {
if (connectionState === 'connecting' && !subscribed) {
return {error: undefined, title: undefined}
}

Expand Down
Loading