Skip to content

Commit

Permalink
feat(core): virtualize review changes screen - corel-91
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Aug 6, 2024
1 parent 306524d commit 4839780
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 73 deletions.
10 changes: 3 additions & 7 deletions packages/sanity/src/core/preview/useObserveDocument.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type ObjectSchemaType, type SanityDocument} from '@sanity/types'
import {type SanityDocument} from '@sanity/types'
import {useMemo} from 'react'
import {useObservable} from 'react-rx'
import {map} from 'rxjs/operators'
Expand All @@ -16,7 +16,6 @@ const INITIAL_STATE = {loading: true, document: null}
*/
export function useObserveDocument<T extends SanityDocument>(
documentId: string,
schemaType: ObjectSchemaType,
): {
document: T | null
loading: boolean
Expand All @@ -25,12 +24,9 @@ export function useObserveDocument<T extends SanityDocument>(
const observable = useMemo(
() =>
documentPreviewStore
.observePaths(
{_id: documentId},
schemaType.fields.map((field) => [field.name]),
)
.unstable_observeDocument(documentId)
.pipe(map((document) => ({loading: false, document: document as T}))),
[documentId, documentPreviewStore, schemaType.fields],
[documentId, documentPreviewStore],
)
return useObservable(observable, INITIAL_STATE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const ReleaseDetail = () => {
documents={results}
release={bundle}
documentsHistory={history.documentsHistory}
scrollContainerRef={scrollContainerRef}
/>
)
}
Expand Down
82 changes: 66 additions & 16 deletions packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {SearchIcon} from '@sanity/icons'
import {Container, Flex, Stack, Text, TextInput} from '@sanity/ui'
import {useMemo, useState} from 'react'
import {Box, Container, Flex, Text, TextInput} from '@sanity/ui'
import {useVirtualizer} from '@tanstack/react-virtual'
import {type RefObject, useCallback, useMemo, useState} from 'react'
import {styled} from 'styled-components'

import {type BundleDocument} from '../../../store/bundles/types'
Expand All @@ -11,16 +12,40 @@ import {type DocumentInBundleResult} from './useBundleDocuments'
const InputContainer = styled(Container)`
margin: 0;
`
const VirtualizerRoot = styled.div`
position: relative;
width: 100%;
`

const VirtualizerTrack = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
`

export function ReleaseReview({
documents,
release,
documentsHistory,
scrollContainerRef,
}: {
documents: DocumentInBundleResult[]
release: BundleDocument
documentsHistory: Record<string, DocumentHistory>
scrollContainerRef: RefObject<HTMLDivElement>
}) {
const [searchTerm, setSearchTerm] = useState('')
const [expandedItems, setIsExpandedItems] = useState<Record<string, boolean>>({})

const toggleIsExpanded = useCallback((documentId: string) => {
setIsExpandedItems((prev) => {
if (typeof prev[documentId] === 'boolean') {
return {...prev, [documentId]: !prev[documentId]}
}
return {...prev, [documentId]: false}
})
}, [])

const filteredList = useMemo(() => {
return documents.filter(({previewValues, document}) => {
Expand All @@ -31,8 +56,17 @@ export function ReleaseReview({
})
}, [searchTerm, documents])

const virtualizer = useVirtualizer({
count: filteredList.length,
getScrollElement: () => scrollContainerRef.current,
// Estimation of a document with 1 change
estimateSize: () => 140,
overscan: 4,
})
const items = virtualizer.getVirtualItems()

return (
<Stack space={5} paddingTop={6}>
<Flex direction="column" gap={5} paddingY={6}>
<Flex justify="space-between" align="center">
<Text size={1} weight="semibold">
Changes to published documents
Expand All @@ -50,18 +84,34 @@ export function ReleaseReview({
/>
</InputContainer>
</Flex>
<Stack space={[5, 6]}>
{filteredList.map(({document, validation, previewValues}) => (
<DocumentDiffContainer
key={document._id}
document={document}
release={release}
history={documentsHistory[document._id]}
validation={validation}
previewValues={previewValues}
/>
))}
</Stack>
</Stack>
<VirtualizerRoot style={{height: virtualizer.getTotalSize()}}>
<VirtualizerTrack style={{transform: `translateY(${items[0]?.start ?? 0}px)`}}>
{items.map((virtualRow) => {
const {document, validation, previewValues} = filteredList[virtualRow.index]

return (
<Box
paddingBottom={[5, 6]}
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
>
<DocumentDiffContainer
key={document._id}
release={release}
history={documentsHistory[document._id]}
document={document}
validation={validation}
previewValues={previewValues}
isExpanded={expandedItems[document._id] ?? true}
// eslint-disable-next-line react/jsx-no-bind
toggleIsExpanded={() => toggleIsExpanded(document._id)}
/>
</Box>
)
})}
</VirtualizerTrack>
</VirtualizerRoot>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
import {act, fireEvent, render, screen} from '@testing-library/react'
import {act, fireEvent, render, screen, within} from '@testing-library/react'
import {ColorSchemeProvider, UserColorManagerProvider} from 'sanity'

import {queryByDataUi} from '../../../../../../test/setup/customQueries'
Expand All @@ -11,26 +11,22 @@ import {type DocumentInBundleResult} from '../useBundleDocuments'

const BASE_DOCUMENTS_MOCKS = {
doc1: {
baseDocument: {
name: 'William Faulkner',
role: 'developer',
_id: 'doc1',
_rev: 'FvEfB9CaLlljeKWNkRBaf5',
_type: 'author',
_createdAt: '',
_updatedAt: '',
},
name: 'William Faulkner',
role: 'developer',
_id: 'doc1',
_rev: 'FvEfB9CaLlljeKWNkRBaf5',
_type: 'author',
_createdAt: '',
_updatedAt: '',
},
doc2: {
baseDocument: {
name: 'Virginia Woolf',
role: 'developer',
_id: 'doc2',
_rev: 'FvEfB9CaLlljeKWNkRBaf5',
_type: 'author',
_createdAt: '',
_updatedAt: '',
},
name: 'Virginia Woolf',
role: 'developer',
_id: 'doc2',
_rev: 'FvEfB9CaLlljeKWNkRBaf5',
_type: 'author',
_createdAt: '',
_updatedAt: '',
},
} as const

Expand Down Expand Up @@ -91,6 +87,7 @@ const MOCKED_DOCUMENTS: DocumentInBundleResult[] = [
},
]
const MOCKED_PROPS = {
scrollContainerRef: {current: null},
documents: MOCKED_DOCUMENTS,
release: {
_updatedAt: '2024-07-12T10:39:32Z',
Expand Down Expand Up @@ -199,7 +196,7 @@ describe('ReleaseReview', () => {
mockedUseObserveDocument.mockImplementation((docId: string) => {
return {
// @ts-expect-error - key is valid, ts won't infer it
document: BASE_DOCUMENTS_MOCKS[docId].baseDocument,
document: BASE_DOCUMENTS_MOCKS[docId],
loading: false,
}
})
Expand Down Expand Up @@ -230,6 +227,37 @@ describe('ReleaseReview', () => {

expect(secondDocumentChange).toBeInTheDocument()
})
it('should collapse documents', () => {
const firstDocumentDiff = screen.getByTestId(
`doc-differences-${MOCKED_DOCUMENTS[0].document._id}`,
)
const secondDocumentDiff = screen.getByTestId(
`doc-differences-${MOCKED_DOCUMENTS[1].document._id}`,
)
expect(within(firstDocumentDiff).getByText('added')).toBeInTheDocument()
expect(within(secondDocumentDiff).getByText('test')).toBeInTheDocument()
// get the toggle button with id 'document-review-header-toggle' inside the first document diff
const firstDocToggle = within(firstDocumentDiff).getByTestId('document-review-header-toggle')
act(() => {
fireEvent.click(firstDocToggle)
})
expect(within(firstDocumentDiff).queryByText('added')).not.toBeInTheDocument()
expect(within(secondDocumentDiff).getByText('test')).toBeInTheDocument()
act(() => {
fireEvent.click(firstDocToggle)
})
expect(within(firstDocumentDiff).getByText('added')).toBeInTheDocument()
expect(within(secondDocumentDiff).getByText('test')).toBeInTheDocument()

const secondDocToggle = within(secondDocumentDiff).getByTestId(
'document-review-header-toggle',
)
act(() => {
fireEvent.click(secondDocToggle)
})
expect(within(firstDocumentDiff).getByText('added')).toBeInTheDocument()
expect(within(secondDocumentDiff).queryByText('test')).not.toBeInTheDocument()
})
})
describe('filtering documents', () => {
beforeEach(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,56 @@ import {DocumentReviewHeader} from '../review/DocumentReviewHeader'
import {type DocumentInBundleResult} from '../useBundleDocuments'
import {DocumentDiff} from './DocumentDiff'

function DocumentDiffExpanded({document}: {document: DocumentInBundleResult['document']}) {
const publishedId = getPublishedId(document._id, true)

const schema = useSchema()
const schemaType = schema.get(document._type) as ObjectSchemaType
if (!schemaType) {
throw new Error(`Schema type "${document._type}" not found`)
}

const {document: baseDocument, loading: baseDocumentLoading} = useObserveDocument(publishedId)

if (baseDocumentLoading) return <LoadingBlock />

return <DocumentDiff baseDocument={baseDocument} document={document} schemaType={schemaType} />
}

export function DocumentDiffContainer({
document,
release,
history,
release,
previewValues,
validation,
isExpanded,
toggleIsExpanded,
}: {
release: BundleDocument
history?: DocumentHistory
document: DocumentInBundleResult['document']
validation?: DocumentInBundleResult['validation']
history?: DocumentHistory
release: BundleDocument
previewValues: DocumentInBundleResult['previewValues']
validation?: DocumentInBundleResult['validation']
isExpanded: boolean
toggleIsExpanded: () => void
}) {
const publishedId = getPublishedId(document._id, true)
const schema = useSchema()
const schemaType = schema.get(document._type) as ObjectSchemaType
if (!schemaType) {
throw new Error(`Schema type "${document._type}" not found`)
}
const {document: baseDocument, loading: baseDocumentLoading} = useObserveDocument(
publishedId,
schemaType,
)

return (
<Card border radius={3}>
<Card border radius={3} data-testid={`doc-differences-${document._id}`}>
<DocumentReviewHeader
document={document}
isLoading={previewValues.isLoading}
previewValues={previewValues.values}
history={history}
release={release}
validation={validation}
isExpanded={isExpanded}
toggleIsExpanded={toggleIsExpanded}
/>
<Flex justify="center" padding={4}>
{baseDocumentLoading ? (
<LoadingBlock />
) : (
<DocumentDiff baseDocument={baseDocument} document={document} schemaType={schemaType} />
)}
</Flex>
{isExpanded && (
<Flex justify="center" padding={4}>
<DocumentDiffExpanded document={document} />
</Flex>
)}
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {ChevronDownIcon, ChevronRightIcon} from '@sanity/icons'
import {type PreviewValue, type SanityDocument} from '@sanity/types'
import {AvatarStack, Box, Card, Flex} from '@sanity/ui'

import {Button} from '../../../../../ui-components'
import {RelativeTime} from '../../../../components/RelativeTime'
import {UserAvatar} from '../../../../components/userAvatar/UserAvatar'
import {type BundleDocument} from '../../../../store/bundles/types'
Expand All @@ -15,32 +17,43 @@ export function DocumentReviewHeader({
history,
release,
validation,
isExpanded,
toggleIsExpanded,
}: {
document: SanityDocument
previewValues: PreviewValue
isLoading: boolean
release: BundleDocument
validation?: DocumentValidationStatus
isExpanded: boolean
toggleIsExpanded: () => void
history?: {
createdBy: string
lastEditedBy: string
editors: string[]
}
}) {
return (
<Card
tone={validation?.hasError ? 'critical' : 'default'}
radius={3}
style={{marginBottom: 20}}
>
<Flex justify="space-between" align="center">
<Box padding={1} style={{flex: '1'}}>
<Card tone={validation?.hasError ? 'critical' : 'default'} radius={3}>
<Flex justify="space-between" align="center" paddingY={1} paddingX={2}>
<Button
data-testid="document-review-header-toggle"
icon={isExpanded ? ChevronDownIcon : ChevronRightIcon}
mode="bleed"
onClick={toggleIsExpanded}
tooltipProps={{
content: isExpanded ? 'Collapse document diff' : 'Expand document diff',
placement: 'top',
}}
/>
<Box style={{flex: '1'}}>
<ReleaseDocumentPreview
documentId={document._id}
documentTypeName={document._type}
releaseSlug={release.slug}
previewValues={previewValues}
isLoading={isLoading}
hasValidationError={validation?.hasError}
/>
</Box>
<Flex gap={2} padding={3} style={{flexShrink: 0}}>
Expand Down

0 comments on commit 4839780

Please sign in to comment.