Skip to content

Commit

Permalink
feat(corel): virtualize content releases plugin lists (#7301)
Browse files Browse the repository at this point in the history
* feat(core): virtualise the content releases lists

* feat(corel): lift preview values state to useBundles hook

* fix(core): update corel sort actions

* fix(core): update useBundleDocuments return value

* fix(core): rename BundleDocumentResult to DocumentInBundleResult

* fix(corel): remove Row prop from releases Table

* fix(core): update how preview is calculated in useBundleDocuments, reuse the fetched document

* feat(core): update corel summary and overview virtual tables

* fix(core): update release summary types import

* feat(core): virtualize review changes screen - corel-91
  • Loading branch information
pedrobonamin authored and juice49 committed Oct 7, 2024
1 parent 69154d4 commit 1e33383
Show file tree
Hide file tree
Showing 18 changed files with 605 additions and 560 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
@@ -1,47 +1,49 @@
import {ErrorOutlineIcon, PublishIcon} from '@sanity/icons'
import {type SanityDocument} from '@sanity/types'
import {Flex, Text, useToast} from '@sanity/ui'
import {useCallback, useMemo, useState} from 'react'
import {type BundleDocument} from 'sanity'

import {Button, Dialog} from '../../../../ui-components'
import {useBundleOperations} from '../../../store/bundles/useBundleOperations'
import {type DocumentValidationStatus} from '../../tool/detail/bundleDocumentsValidation'
import {type DocumentInBundleResult} from '../../tool/detail/useBundleDocuments'
import {useObserveDocumentRevisions} from './useObserveDocumentRevisions'

interface ReleasePublishAllButtonProps {
bundle: BundleDocument
bundleDocuments: SanityDocument[]
bundleDocuments: DocumentInBundleResult[]
disabled?: boolean
validation: Record<string, DocumentValidationStatus>
}

export const ReleasePublishAllButton = ({
bundle,
bundleDocuments,
disabled,
validation,
}: ReleasePublishAllButtonProps) => {
const toast = useToast()
const {publishBundle} = useBundleOperations()
const [publishBundleStatus, setPublishBundleStatus] = useState<'idle' | 'confirm' | 'publishing'>(
'idle',
)

const publishedDocumentsRevisions = useObserveDocumentRevisions(bundleDocuments)
const publishedDocumentsRevisions = useObserveDocumentRevisions(
bundleDocuments.map(({document}) => document),
)

const isValidatingDocuments = Object.values(validation).some(({isValidating}) => isValidating)
const hasDocumentValidationErrors = Object.values(validation).some(({hasError}) => hasError)
const isValidatingDocuments = bundleDocuments.some(({validation}) => validation.isValidating)
const hasDocumentValidationErrors = bundleDocuments.some(({validation}) => validation.hasError)

const isPublishButtonDisabled =
disabled || isValidatingDocuments || hasDocumentValidationErrors || !publishedDocumentsRevisions
const isPublishButtonDisabled = disabled || isValidatingDocuments || hasDocumentValidationErrors

const handleConfirmPublishAll = useCallback(async () => {
if (!bundle || !publishedDocumentsRevisions) return

try {
setPublishBundleStatus('publishing')
await publishBundle(bundle._id, bundleDocuments, publishedDocumentsRevisions)
await publishBundle(
bundle._id,
bundleDocuments.map(({document}) => document),
publishedDocumentsRevisions,
)
toast.push({
closable: true,
status: 'success',
Expand Down
166 changes: 117 additions & 49 deletions packages/sanity/src/core/releases/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {Box, Card, Flex, Stack, Text} from '@sanity/ui'
import {
defaultRangeExtractor,
type Range,
useVirtualizer,
type VirtualItem,
} from '@tanstack/react-virtual'
import {get} from 'lodash'
import {Fragment, useMemo} from 'react'
import {Fragment, type MutableRefObject, useMemo, useRef} from 'react'
import {styled} from 'styled-components'

import {TooltipDelayGroupProvider} from '../../../../ui-components'
Expand All @@ -16,49 +22,75 @@ type RowDatum<TableData, AdditionalRowTableData> = AdditionalRowTableData extend
export interface TableProps<TableData, AdditionalRowTableData> {
columnDefs: Column<RowDatum<TableData, AdditionalRowTableData>>[]
searchFilter?: (data: TableData[], searchTerm: string) => TableData[]
Row?: ({
datum,
children,
}: {
datum: TableData
children: (rowData: TableData) => JSX.Element
}) => JSX.Element | null
data: TableData[]
emptyState: (() => JSX.Element) | string
loading?: boolean
rowId: keyof TableData
/**
* Should be the dot separated path to the unique identifier of the row. e.g. document._id
*/
rowId: string
rowActions?: ({
datum,
}: {
datum: RowDatum<TableData, AdditionalRowTableData> | unknown
}) => JSX.Element
scrollContainerRef: MutableRefObject<HTMLDivElement | null>
}

const RowStack = styled(Stack)({
'& > *:not(:first-child)': {
'& > *:not([first-child])': {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
marginTop: -1,
},

'& > *:not(:last-child)': {
'& > *:not([last-child])': {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
})

const ITEM_HEIGHT = 59

/**
* This function modifies the rangeExtractor to account for the offset of the virtualizer
* in this case, the parent with overflow (the element over which the scroll happens) and the start of the virtualizer
* don't match, because there are some elements rendered on top of the virtualizer.
* This, will take care of adding more elements to the start of the virtualizer to account for the offset.
*/
const withVirtualizerOffset = ({
scrollContainerRef,
virtualizerContainerRef,
range,
}: {
scrollContainerRef: MutableRefObject<HTMLDivElement | null>
virtualizerContainerRef: MutableRefObject<HTMLDivElement | null>
range: Range
}) => {
const parentOffset = scrollContainerRef.current?.offsetTop ?? 0
const virtualizerOffset = virtualizerContainerRef.current?.offsetTop ?? 0
const virtualizerScrollMargin = virtualizerOffset - parentOffset
const topItemsOffset = Math.ceil(virtualizerScrollMargin / ITEM_HEIGHT)
const startIndexWithOffset = range.startIndex - topItemsOffset
const result = defaultRangeExtractor({
...range,
// By modifying the startIndex, we are adding more elements to the start of the virtualizer
startIndex: startIndexWithOffset > 0 ? startIndexWithOffset : 0,
})
return result
}
const TableInner = <TableData, AdditionalRowTableData>({
columnDefs,
data,
emptyState,
searchFilter,
Row,
rowId,
rowActions,
loading = false,
scrollContainerRef,
}: TableProps<TableData, AdditionalRowTableData>) => {
const {searchTerm, sort} = useTableContext()

const virtualizerContainerRef = useRef<HTMLDivElement | null>(null)
const filteredData = useMemo(() => {
const filteredResult = searchTerm && searchFilter ? searchFilter(data, searchTerm) : data
if (!sort) return filteredResult
Expand All @@ -76,6 +108,15 @@ const TableInner = <TableData, AdditionalRowTableData>({
})
}, [data, searchFilter, searchTerm, sort])

const rowVirtualizer = useVirtualizer({
count: filteredData.length,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => ITEM_HEIGHT,
overscan: 5,
rangeExtractor: (range) =>
withVirtualizerOffset({scrollContainerRef, virtualizerContainerRef, range}),
})

const rowActionColumnDef: Column = useMemo(
() => ({
id: 'actions',
Expand Down Expand Up @@ -106,16 +147,29 @@ const TableInner = <TableData, AdditionalRowTableData>({

const renderRow = useMemo(
() =>
function TableRow(datum: TableData | (TableData & AdditionalRowTableData)) {
function TableRow(
datum: (TableData | (TableData & AdditionalRowTableData)) & {
virtualRow: VirtualItem
index: number
isFirst: boolean
isLast: boolean
},
) {
return (
<Card
key={String(datum[rowId])}
key={String(get(datum, rowId))}
data-testid="table-row"
as="tr"
border
radius={3}
display="flex"
first-child={datum.isFirst ? '' : undefined}
last-child={datum.isLast ? '' : undefined}
margin={-1}
style={{
height: `${datum.virtualRow.size}px`,
transform: `translateY(${datum.virtualRow.start - datum.index * datum.virtualRow.size}px)`,
}}
// @ts-expect-error - Using a custom datum prop, this is not definitive, just a placeholder to show there is an error.
// update once designs land
tone={datum?.validation?.hasError ? 'critical' : 'default'}
Expand All @@ -139,38 +193,27 @@ const TableInner = <TableData, AdditionalRowTableData>({
[amalgamatedColumnDefs, rowId],
)

const tableContent = useMemo(() => {
if (filteredData.length === 0) {
if (typeof emptyState === 'string') {
return (
<Card
as="tr"
border
radius={3}
display="flex"
padding={4}
style={{
justifyContent: 'center',
}}
>
<Text as="td" muted size={1}>
{emptyState}
</Text>
</Card>
)
}
return emptyState()
}

return filteredData.map((datum) => {
if (!Row) return renderRow(datum)
const emptyContent = useMemo(() => {
if (typeof emptyState === 'string') {
return (
<Row key={String(datum[rowId])} datum={datum}>
{renderRow}
</Row>
<Card
as="tr"
border
radius={3}
display="flex"
padding={4}
style={{
justifyContent: 'center',
}}
>
<Text as="td" muted size={1}>
{emptyState}
</Text>
</Card>
)
})
}, [Row, emptyState, filteredData, renderRow, rowId])
}
return emptyState()
}, [emptyState])

const headers = useMemo(
() => amalgamatedColumnDefs.map(({cell, ...header}) => ({...header, id: String(header.id)})),
Expand All @@ -182,10 +225,35 @@ const TableInner = <TableData, AdditionalRowTableData>({
}

return (
<Stack as="table" space={1}>
<TableHeader headers={headers} searchDisabled={!searchTerm && !data.length} />
<RowStack as="tbody">{tableContent}</RowStack>
</Stack>
<div ref={virtualizerContainerRef}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
// The padding accounts for the height of the <TableHeader> and extra space for padding at the bottom
paddingBottom: '110px',
width: '100%',
position: 'relative',
}}
>
<Stack as="table" space={1}>
<TableHeader headers={headers} searchDisabled={!searchTerm && !data.length} />
<RowStack as="tbody">
{filteredData.length === 0
? emptyContent
: rowVirtualizer.getVirtualItems().map((virtualRow, index) => {
const datum = filteredData[virtualRow.index]
return renderRow({
...datum,
virtualRow,
index,
isFirst: virtualRow.index === 0,
isLast: virtualRow.index === filteredData.length - 1,
})
})}
</RowStack>
</Stack>
</div>
</div>
)
}

Expand Down
Loading

0 comments on commit 1e33383

Please sign in to comment.