diff --git a/packages/sanity/src/core/preview/useObserveDocument.ts b/packages/sanity/src/core/preview/useObserveDocument.ts index 987784a9b188..7d3862659842 100644 --- a/packages/sanity/src/core/preview/useObserveDocument.ts +++ b/packages/sanity/src/core/preview/useObserveDocument.ts @@ -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' @@ -16,7 +16,6 @@ const INITIAL_STATE = {loading: true, document: null} */ export function useObserveDocument( documentId: string, - schemaType: ObjectSchemaType, ): { document: T | null loading: boolean @@ -25,12 +24,9 @@ export function useObserveDocument( 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) } diff --git a/packages/sanity/src/core/releases/components/ReleasePublishAllButton/ReleasePublishAllButton.tsx b/packages/sanity/src/core/releases/components/ReleasePublishAllButton/ReleasePublishAllButton.tsx index baa26105673d..d0ca3b0322dc 100644 --- a/packages/sanity/src/core/releases/components/ReleasePublishAllButton/ReleasePublishAllButton.tsx +++ b/packages/sanity/src/core/releases/components/ReleasePublishAllButton/ReleasePublishAllButton.tsx @@ -1,26 +1,23 @@ 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 } export const ReleasePublishAllButton = ({ bundle, bundleDocuments, disabled, - validation, }: ReleasePublishAllButtonProps) => { const toast = useToast() const {publishBundle} = useBundleOperations() @@ -28,20 +25,25 @@ export const ReleasePublishAllButton = ({ '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', diff --git a/packages/sanity/src/core/releases/components/Table/Table.tsx b/packages/sanity/src/core/releases/components/Table/Table.tsx index 1265e6a69934..80f4726b22b1 100644 --- a/packages/sanity/src/core/releases/components/Table/Table.tsx +++ b/packages/sanity/src/core/releases/components/Table/Table.tsx @@ -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' @@ -16,49 +22,75 @@ type RowDatum = AdditionalRowTableData extend export interface TableProps { columnDefs: Column>[] 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 | unknown }) => JSX.Element + scrollContainerRef: MutableRefObject } 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 + virtualizerContainerRef: MutableRefObject + 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 = ({ columnDefs, data, emptyState, searchFilter, - Row, rowId, rowActions, loading = false, + scrollContainerRef, }: TableProps) => { const {searchTerm, sort} = useTableContext() - + const virtualizerContainerRef = useRef(null) const filteredData = useMemo(() => { const filteredResult = searchTerm && searchFilter ? searchFilter(data, searchTerm) : data if (!sort) return filteredResult @@ -76,6 +108,15 @@ const TableInner = ({ }) }, [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', @@ -106,16 +147,29 @@ const TableInner = ({ const renderRow = useMemo( () => - function TableRow(datum: TableData | (TableData & AdditionalRowTableData)) { + function TableRow( + datum: (TableData | (TableData & AdditionalRowTableData)) & { + virtualRow: VirtualItem + index: number + isFirst: boolean + isLast: boolean + }, + ) { return ( ({ [amalgamatedColumnDefs, rowId], ) - const tableContent = useMemo(() => { - if (filteredData.length === 0) { - if (typeof emptyState === 'string') { - return ( - - - {emptyState} - - - ) - } - return emptyState() - } - - return filteredData.map((datum) => { - if (!Row) return renderRow(datum) + const emptyContent = useMemo(() => { + if (typeof emptyState === 'string') { return ( - - {renderRow} - + + + {emptyState} + + ) - }) - }, [Row, emptyState, filteredData, renderRow, rowId]) + } + return emptyState() + }, [emptyState]) const headers = useMemo( () => amalgamatedColumnDefs.map(({cell, ...header}) => ({...header, id: String(header.id)})), @@ -182,10 +225,35 @@ const TableInner = ({ } return ( - - - {tableContent} - +
+
and extra space for padding at the bottom + paddingBottom: '110px', + width: '100%', + position: 'relative', + }} + > + + + + {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, + }) + })} + + +
+
) } diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx index 9c4acac722b1..fab6ce8879e7 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx @@ -1,6 +1,7 @@ import {ArrowLeftIcon} from '@sanity/icons' import {Box, Card, Container, Flex, Heading, Stack, Text} from '@sanity/ui' -import {useCallback, useEffect, useMemo} from 'react' +// eslint-disable-next-line camelcase +import {useCallback, useEffect, useMemo, useRef} from 'react' import {LoadingBlock} from 'sanity' import {type RouterContextValue, useRouter} from 'sanity/router' @@ -9,7 +10,6 @@ import {useBundles} from '../../../store/bundles' import {BundleMenuButton} from '../../components/BundleMenuButton/BundleMenuButton' import {ReleasePublishAllButton} from '../../components/ReleasePublishAllButton/ReleasePublishAllButton' import {type ReleasesRouterState} from '../../types/router' -import {type DocumentValidationStatus} from './bundleDocumentsValidation' import {useReleaseHistory} from './documentTable/useReleaseHistory' import {ReleaseReview} from './ReleaseReview' import {ReleaseSummary} from './ReleaseSummary' @@ -29,6 +29,7 @@ const getActiveScreen = (router: RouterContextValue): Screen => { } return activeScreen } + export const ReleaseDetail = () => { const router = useRouter() @@ -39,16 +40,12 @@ export const ReleaseDetail = () => { const {data, loading} = useBundles() const {loading: documentsLoading, results} = useBundleDocuments(parsedSlug) - const bundleDocuments = results.map((result) => result.document) - const documentIds = results.map((result) => result.document?._id) + const documentIds = results.map((result) => result.document?._id) const history = useReleaseHistory(documentIds) - const validation: Record = Object.fromEntries( - results.map((result) => [result.document._id, result.validation]), - ) const bundle = data?.find((storeBundle) => storeBundle.slug === parsedSlug) - const bundleHasDocuments = !!documentIds.length + const bundleHasDocuments = !!results.length const showPublishButton = loading || !bundle?.publishedAt const isPublishButtonDisabled = loading || !bundle || !bundleHasDocuments @@ -76,16 +73,7 @@ export const ReleaseDetail = () => { const header = useMemo( () => ( - + @@ -136,12 +124,11 @@ export const ReleaseDetail = () => { {showPublishButton && bundle && ( )} - + @@ -149,16 +136,16 @@ export const ReleaseDetail = () => { [ activeScreen, bundle, - bundleDocuments, bundleHasDocuments, isPublishButtonDisabled, navigateToReview, navigateToSummary, + results, router, showPublishButton, - validation, ], ) + const scrollContainerRef = useRef(null) const detailContent = useMemo(() => { if (!bundle) return null @@ -166,33 +153,26 @@ export const ReleaseDetail = () => { if (activeScreen === 'summary') { return ( ) } if (activeScreen === 'review') { return ( ) } return null - }, [ - activeScreen, - bundle, - bundleDocuments, - history.collaborators, - history.documentsHistory, - validation, - ]) + }, [activeScreen, bundle, history.collaborators, history.documentsHistory, results]) if (loading) { return @@ -211,16 +191,13 @@ export const ReleaseDetail = () => { } return ( - + {header} - - - - - {documentsLoading ? : detailContent} - + + + {documentsLoading ? : detailContent} - + ) } diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx index c6d5d466d4db..5e298ca4b2f9 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx @@ -1,32 +1,74 @@ import {SearchIcon} from '@sanity/icons' -import {type SanityDocument} from '@sanity/types' -import {Container, Flex, Stack, Text, TextInput} from '@sanity/ui' -import {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' -import {type DocumentValidationStatus} from './bundleDocumentsValidation' import {type DocumentHistory} from './documentTable/useReleaseHistory' import {DocumentDiffContainer} from './review/DocumentDiffContainer' +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%; +` + +// Estimation of a document with 1 change +const REVIEW_ITEM_ESTIMATED_HEIGHT = 140 + export function ReleaseReview({ documents, release, documentsHistory, - validation, + scrollContainerRef, }: { - documents: SanityDocument[] + documents: DocumentInBundleResult[] release: BundleDocument documentsHistory: Record - validation: Record + scrollContainerRef: RefObject }) { const [searchTerm, setSearchTerm] = useState('') + const [expandedItems, setIsExpandedItems] = useState>({}) + + 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}) => { + const fallbackTitle = typeof document.title === 'string' ? document.title : 'Untitled' + const title = + typeof previewValues.values.title === 'string' ? previewValues.values.title : fallbackTitle + return title.toLowerCase().includes(searchTerm.toLowerCase()) + }) + }, [searchTerm, documents]) + + const virtualizer = useVirtualizer({ + count: filteredList.length, + getScrollElement: () => scrollContainerRef.current, + estimateSize: () => REVIEW_ITEM_ESTIMATED_HEIGHT, + overscan: 4, + }) + const items = virtualizer.getVirtualItems() return ( - + Changes to published documents @@ -44,18 +86,34 @@ export function ReleaseReview({ /> - - {documents.map((document) => ( - - ))} - - + + + {items.map((virtualRow) => { + const {document, validation, previewValues} = filteredList[virtualRow.index] + + return ( + + toggleIsExpanded(document._id)} + /> + + ) + })} + + + ) } diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseSummary.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseSummary.tsx index f6a8f185a95f..2ea4bdfb699d 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseSummary.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseSummary.tsx @@ -1,7 +1,6 @@ import {DocumentsIcon} from '@sanity/icons' -import {type SanityDocument} from '@sanity/types' import {AvatarStack, Box, Flex, Heading, Stack, Text, useToast} from '@sanity/ui' -import {useCallback, useEffect, useMemo, useState} from 'react' +import {type RefObject, useCallback, useEffect, useMemo, useState} from 'react' import { BundleIconEditorPicker, @@ -12,53 +11,32 @@ import {UserAvatar} from '../../../components/userAvatar/UserAvatar' import {type BundleDocument} from '../../../store/bundles/types' import {useAddonDataset} from '../../../studio/addonDataset/useAddonDataset' import {Chip} from '../../components/Chip' -import {Table, type TableProps} from '../../components/Table/Table' -import {useTableContext} from '../../components/Table/TableProvider' -import {type DocumentValidationStatus} from './bundleDocumentsValidation' +import {Table} from '../../components/Table/Table' import {DocumentActions} from './documentTable/DocumentActions' import {getDocumentTableColumnDefs} from './documentTable/DocumentTableColumnDefs' -import {useDocumentPreviewValues} from './documentTable/useDocumentPreviewValues' import {type DocumentHistory} from './documentTable/useReleaseHistory' +import {type DocumentInBundleResult} from './useBundleDocuments' -export type DocumentWithHistory = SanityDocument & { +export type DocumentWithHistory = DocumentInBundleResult & { history: DocumentHistory | undefined - validation: DocumentValidationStatus | undefined } -export type BundleDocumentRow = DocumentWithHistory & ReturnType +export type BundleDocumentRow = DocumentWithHistory export interface ReleaseSummaryProps { - documents: SanityDocument[] + documents: DocumentInBundleResult[] documentsHistory: Record collaborators: string[] + scrollContainerRef: RefObject release: BundleDocument - validation: Record } -const getRow = - ( - release: BundleDocument, - ): TableProps>['Row'] => - ({children, datum}) => { - const {searchTerm} = useTableContext() - const {previewValues, isLoading} = useDocumentPreviewValues({document: datum, release}) - - if (searchTerm) { - // Early return to filter out documents that don't match the search term - const fallbackTitle = typeof document.title === 'string' ? document.title : 'Untitled' - const title = typeof previewValues.title === 'string' ? previewValues.title : fallbackTitle - if (!title.toLowerCase().includes(searchTerm.toLowerCase())) return null - } - - return children({...datum, previewValues, isLoading}) - } - const setIconHue = ({hue, icon}: {hue: BundleDocument['hue']; icon: BundleDocument['icon']}) => ({ hue: hue ?? 'gray', icon: icon ?? 'documents', }) export function ReleaseSummary(props: ReleaseSummaryProps) { - const {documents, documentsHistory, release, collaborators, validation} = props + const {documents, documentsHistory, release, collaborators, scrollContainerRef} = props const {hue, icon} = release const {client} = useAddonDataset() @@ -94,14 +72,11 @@ export function ReleaseSummary(props: ReleaseSummaryProps) { () => documents.map((document) => ({ ...document, - history: documentsHistory[document._id], - validation: validation[document._id], + history: documentsHistory[document.document._id], })), - [documents, documentsHistory, validation], + [documents, documentsHistory], ) - const Row = useMemo(() => getRow(release), [release]) - const renderRowActions: ({datum}: {datum: BundleDocumentRow | unknown}) => JSX.Element = useCallback( ({datum}) => { @@ -119,9 +94,19 @@ export function ReleaseSummary(props: ReleaseSummaryProps) { // update hue and icon when release changes useEffect(() => setIconValue(setIconHue({hue, icon})), [hue, icon]) + const filterRows = useCallback( + (data: DocumentWithHistory[], searchTerm: string) => + data.filter(({previewValues}) => { + const title = + typeof previewValues.values.title === 'string' ? previewValues.values.title : 'Untitled' + return title.toLowerCase().includes(searchTerm.toLowerCase()) + }), + [], + ) + return ( - - + <> + @@ -187,14 +172,15 @@ export function ReleaseSummary(props: ReleaseSummaryProps) { - > + data={aggregatedData} emptyState="No documents" - rowId="_id" - Row={Row} + rowId="document._id" columnDefs={documentTableColumnDefs} rowActions={renderRowActions} + searchFilter={filterRows} + scrollContainerRef={scrollContainerRef} /> - + ) } diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetail.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetail.test.tsx index 4a9fbe45666b..207ba5c15263 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetail.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetail.test.tsx @@ -219,11 +219,14 @@ describe('after bundles have loaded', () => { _updatedAt: currentDate, }, validation: { - documentId: '123', hasError: false, isValidating: true, validation: [], }, + previewValues: { + values: {title: 'Test document'}, + isLoading: false, + }, }, ], }) @@ -252,11 +255,14 @@ describe('after bundles have loaded', () => { _updatedAt: currentDate, }, validation: { - documentId: '123', hasError: false, isValidating: false, validation: [], }, + previewValues: { + values: {title: 'Test document'}, + isLoading: false, + }, }, ], }) @@ -312,7 +318,6 @@ describe('after bundles have loaded', () => { _updatedAt: currentDate, }, validation: { - documentId: '123', hasError: true, isValidating: false, validation: [ @@ -323,6 +328,10 @@ describe('after bundles have loaded', () => { }, ], }, + previewValues: { + values: {title: 'Test document'}, + isLoading: false, + }, }, ], }) diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx index d49408bce602..cb43958c9184 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx @@ -1,61 +1,38 @@ import {beforeEach, describe, expect, it, jest} from '@jest/globals' -import {type SanityDocument} from '@sanity/client' -import {act, fireEvent, render, screen} from '@testing-library/react' -import {ColorSchemeProvider, getPublishedId, UserColorManagerProvider} from 'sanity' +import {act, fireEvent, render, screen, within} from '@testing-library/react' +import {ColorSchemeProvider, UserColorManagerProvider} from 'sanity' import {queryByDataUi} from '../../../../../../test/setup/customQueries' import {createWrapper} from '../../../../../../test/testUtils/createWrapper' import {useObserveDocument} from '../../../../preview/useObserveDocument' import {releasesUsEnglishLocaleBundle} from '../../../i18n' -import {useDocumentPreviewValues} from '../documentTable/useDocumentPreviewValues' import {ReleaseReview} from '../ReleaseReview' +import {type DocumentInBundleResult} from '../useBundleDocuments' -const DOCUMENTS_MOCKS = { +const BASE_DOCUMENTS_MOCKS = { doc1: { - baseDocument: { - name: 'William Faulkner', - role: 'developer', - _id: 'doc1', - _rev: 'FvEfB9CaLlljeKWNkRBaf5', - _type: 'author', - _createdAt: '', - _updatedAt: '', - }, - previewValues: { - _id: 'differences.doc1', - _type: 'author', - _createdAt: '2024-07-10T12:10:38Z', - _updatedAt: '2024-07-15T10:46:02Z', - _version: {}, - title: 'William Faulkner added', - subtitle: 'Designer', - }, + 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: '', - }, - previewValues: { - _id: 'differences.doc2', - _type: 'author', - _createdAt: '2024-07-10T12:10:38Z', - _updatedAt: '2024-07-15T10:46:02Z', - _version: {}, - title: 'Virginia Woolf test', - subtitle: 'Developer', - }, + name: 'Virginia Woolf', + role: 'developer', + _id: 'doc2', + _rev: 'FvEfB9CaLlljeKWNkRBaf5', + _type: 'author', + _createdAt: '', + _updatedAt: '', }, } as const -const MOCKED_PROPS = { - documents: [ - { +const MOCKED_DOCUMENTS: DocumentInBundleResult[] = [ + { + document: { _rev: 'FvEfB9CaLlljeKWNkQgpz9', _type: 'author', role: 'designer', @@ -64,7 +41,25 @@ const MOCKED_PROPS = { _id: 'differences.doc1', _updatedAt: '2024-07-15T10:46:02Z', }, - { + previewValues: { + isLoading: false, + values: { + _createdAt: '2024-07-10T12:10:38Z', + _updatedAt: '2024-07-15T10:46:02Z', + _version: {}, + title: 'William Faulkner added', + subtitle: 'Designer', + }, + }, + validation: { + isValidating: false, + validation: [], + revision: 'FvEfB9CaLlljeKWNk8Mh0N', + hasError: false, + }, + }, + { + document: { _rev: 'FvEfB9CaLlljeKWNkQg1232', _type: 'author', role: 'developer', @@ -73,7 +68,27 @@ const MOCKED_PROPS = { _id: 'differences.doc2', _updatedAt: '2024-07-15T10:46:02Z', }, - ], + previewValues: { + isLoading: false, + values: { + _createdAt: '2024-07-10T12:10:38Z', + _updatedAt: '2024-07-15T10:46:02Z', + _version: {}, + title: 'Virginia Woolf test', + subtitle: 'Developer', + }, + }, + validation: { + isValidating: false, + validation: [], + revision: 'FvEfB9CaLlljeKWNk8Mh0N', + hasError: false, + }, + }, +] +const MOCKED_PROPS = { + scrollContainerRef: {current: null}, + documents: MOCKED_DOCUMENTS, release: { _updatedAt: '2024-07-12T10:39:32Z', authorId: 'p8xDvUMxC', @@ -102,22 +117,6 @@ const MOCKED_PROPS = { editors: ['p8xDvUMxC'], }, }, - validation: { - 'differences.doc1': { - isValidating: false, - validation: [], - revision: 'FvEfB9CaLlljeKWNk8Mh0N', - documentId: 'differences.doc1', - hasError: false, - }, - 'differences.doc2': { - isValidating: false, - validation: [], - revision: 'FvEfB9CaLlljeKWNk8Mh0N', - documentId: 'differences.doc1', - hasError: false, - }, - }, } jest.mock('sanity/router', () => ({ @@ -135,16 +134,7 @@ jest.mock('../../../../preview/useObserveDocument', () => { } }) -jest.mock('../documentTable/useDocumentPreviewValues', () => { - return { - useDocumentPreviewValues: jest.fn(), - } -}) - const mockedUseObserveDocument = useObserveDocument as jest.Mock -const mockedUseDocumentPreviewValues = useDocumentPreviewValues as jest.Mock< - typeof useDocumentPreviewValues -> describe('ReleaseReview', () => { describe('when loading baseDocument', () => { @@ -153,10 +143,6 @@ describe('ReleaseReview', () => { document: null, loading: true, }) - mockedUseDocumentPreviewValues.mockReturnValue({ - previewValues: DOCUMENTS_MOCKS.doc1.previewValues, - isLoading: false, - }) const wrapper = await createWrapper({ resources: [releasesUsEnglishLocaleBundle], }) @@ -174,10 +160,6 @@ describe('ReleaseReview', () => { document: null, loading: false, }) - mockedUseDocumentPreviewValues.mockReturnValue({ - previewValues: DOCUMENTS_MOCKS.doc1.previewValues, - isLoading: false, - }) const wrapper = await createWrapper({ resources: [releasesUsEnglishLocaleBundle], }) @@ -193,13 +175,10 @@ describe('ReleaseReview', () => { describe('when the base document is loaded and there are no changes', () => { beforeEach(async () => { mockedUseObserveDocument.mockReturnValue({ - document: MOCKED_PROPS.documents[0], + document: MOCKED_DOCUMENTS[0].document, loading: false, }) - mockedUseDocumentPreviewValues.mockReturnValue({ - previewValues: DOCUMENTS_MOCKS.doc1.previewValues, - isLoading: false, - }) + const wrapper = await createWrapper({ resources: [releasesUsEnglishLocaleBundle], }) @@ -217,19 +196,11 @@ describe('ReleaseReview', () => { mockedUseObserveDocument.mockImplementation((docId: string) => { return { // @ts-expect-error - key is valid, ts won't infer it - document: DOCUMENTS_MOCKS[docId].baseDocument, + document: BASE_DOCUMENTS_MOCKS[docId], loading: false, } }) - mockedUseDocumentPreviewValues.mockImplementation( - ({document}: {document: SanityDocument}) => { - return { - // @ts-expect-error - key is valid, ts won't infer it - previewValues: DOCUMENTS_MOCKS[getPublishedId(document._id, true)].previewValues, - isLoading: false, - } - }, - ) + const wrapper = await createWrapper({ resources: [releasesUsEnglishLocaleBundle], }) @@ -256,20 +227,42 @@ 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 () => { mockedUseObserveDocument.mockReturnValue({document: null, loading: false}) - mockedUseDocumentPreviewValues.mockImplementation( - ({document}: {document: SanityDocument}) => { - return { - // @ts-expect-error - key is valid, ts won't infer it - previewValues: DOCUMENTS_MOCKS[getPublishedId(document._id, true)].previewValues, - isLoading: false, - } - }, - ) const wrapper = await createWrapper({ resources: [releasesUsEnglishLocaleBundle], }) @@ -277,8 +270,12 @@ describe('ReleaseReview', () => { }) it('should show all the documents when no filter is applied', () => { - expect(screen.queryByText(DOCUMENTS_MOCKS.doc1.previewValues.title)).toBeInTheDocument() - expect(screen.queryByText(DOCUMENTS_MOCKS.doc2.previewValues.title)).toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[0].previewValues.values.title as string), + ).toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[1].previewValues.values.title as string), + ).toBeInTheDocument() }) it('should show support filtering by title', () => { const searchInput = screen.getByPlaceholderText('Search documents') @@ -286,14 +283,22 @@ describe('ReleaseReview', () => { fireEvent.change(searchInput, {target: {value: 'Virginia'}}) }) - expect(screen.queryByText(DOCUMENTS_MOCKS.doc1.previewValues.title)).not.toBeInTheDocument() - expect(screen.queryByText(DOCUMENTS_MOCKS.doc2.previewValues.title)).toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[0].previewValues.values.title as string), + ).not.toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[1].previewValues.values.title as string), + ).toBeInTheDocument() act(() => { fireEvent.change(searchInput, {target: {value: ''}}) }) - expect(screen.queryByText(DOCUMENTS_MOCKS.doc1.previewValues.title)).toBeInTheDocument() - expect(screen.queryByText(DOCUMENTS_MOCKS.doc2.previewValues.title)).toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[0].previewValues.values.title as string), + ).toBeInTheDocument() + expect( + screen.queryByText(MOCKED_DOCUMENTS[1].previewValues.values.title as string), + ).toBeInTheDocument() }) }) }) diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx index 7b7dcc8aa9e0..6a6dc123fe8a 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx @@ -1,5 +1,4 @@ import {beforeEach, describe, expect, it, jest} from '@jest/globals' -import {type SanityDocument} from '@sanity/types' import {fireEvent, render, screen, within} from '@testing-library/react' import {type BundleDocument, defineType} from 'sanity' import {route, RouterProvider} from 'sanity/router' @@ -8,17 +7,12 @@ import {getAllByDataUi, getByDataUi} from '../../../../../../test/setup/customQu import {createWrapper} from '../../../../../../test/testUtils/createWrapper' import {type DocumentHistory} from '../documentTable/useReleaseHistory' import {ReleaseSummary, type ReleaseSummaryProps} from '../ReleaseSummary' +import {type DocumentInBundleResult} from '../useBundleDocuments' jest.mock('../../../../studio/addonDataset/useAddonDataset', () => ({ useAddonDataset: jest.fn().mockReturnValue({client: {}}), })) -jest.mock('../documentTable/useDocumentPreviewValues', () => ({ - useDocumentPreviewValues: ({document}: {document: SanityDocument}) => ({ - previewValues: {title: document?.title}, - }), -})) - jest.mock('../../../../store', () => ({ ...(jest.requireActual('../../../../store') || {}), useUser: jest.fn().mockReturnValue([{}]), @@ -30,28 +24,54 @@ jest.mock('../../../../user-color', () => ({ const timeNow = new Date() -const releaseDocuments = [ +const releaseDocuments: DocumentInBundleResult[] = [ { - _id: '123', - _type: 'document', - // 3 days ago - _createdAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000 * 3).toISOString(), - // 2 days ago - _updatedAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000 * 2).toISOString(), - _version: {}, - _rev: 'abc', - title: 'First document', + document: { + _id: '123', + _type: 'document', + // 3 days ago + _createdAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000 * 3).toISOString(), + // 2 days ago + _updatedAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000 * 2).toISOString(), + _version: {}, + _rev: 'abc', + title: 'First document', + }, + previewValues: { + values: { + title: 'First document', + }, + isLoading: false, + }, + validation: { + hasError: false, + isValidating: true, + validation: [], + }, }, { - _id: '456', - _type: 'document', - // 24 hrs ago - _createdAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000).toISOString(), - // 12 hrs ago - _updatedAt: new Date(timeNow.getTime() - 12 * 60 * 60 * 1000).toISOString(), - _version: {}, - _rev: 'abc', - title: 'Second document', + document: { + _id: '456', + _type: 'document', + // 24 hrs ago + _createdAt: new Date(timeNow.getTime() - 24 * 60 * 60 * 1000).toISOString(), + // 12 hrs ago + _updatedAt: new Date(timeNow.getTime() - 12 * 60 * 60 * 1000).toISOString(), + _version: {}, + _rev: 'abc', + title: 'Second document', + }, + previewValues: { + values: { + title: 'Second document', + }, + isLoading: false, + }, + validation: { + hasError: false, + isValidating: true, + validation: [], + }, }, ] @@ -87,6 +107,7 @@ const renderTest = async (props: Partial) => { router={route.create('/test', [route.intents('/intent')])} > ) => { authorId: 'author-id', } as BundleDocument } - validation={{}} {...props} /> , @@ -147,7 +167,7 @@ describe('ReleaseSummary', () => { }) describe('documents table', () => { - it.only('shows list of all documents in release', () => { + it('shows list of all documents in release', () => { const documents = screen.getAllByTestId('table-row') expect(documents).toHaveLength(2) diff --git a/packages/sanity/src/core/releases/tool/detail/bundleDocumentsValidation.ts b/packages/sanity/src/core/releases/tool/detail/bundleDocumentsValidation.ts deleted file mode 100644 index ead17e2b166c..000000000000 --- a/packages/sanity/src/core/releases/tool/detail/bundleDocumentsValidation.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {isValidationErrorMarker, type SanityDocument, type Schema} from '@sanity/types' -import {omit} from 'lodash' -import {combineLatest, type Observable} from 'rxjs' -import {distinctUntilChanged, map, scan, shareReplay} from 'rxjs/operators' -import { - type DraftsModelDocumentAvailability, - type LocaleSource, - type SanityClient, - type SourceClientOptions, -} from 'sanity' -import shallowEquals from 'shallow-equals' - -import {validateDocumentWithReferences, type ValidationStatus} from '../../../validation' - -function shareLatestWithRefCount() { - return shareReplay({bufferSize: 1, refCount: true}) -} - -export interface DocumentValidationStatus extends ValidationStatus { - documentId: string - hasError: boolean -} - -/** - * It takes a list of document IDs and returns an observable of validation status for each document, indicating if it has - * an error and adds the documentId to the result. - */ -export const bundleDocumentsValidation = ( - ctx: { - observeDocumentPairAvailability: (id: string) => Observable - observeDocument: (id: string) => Observable - getClient: (options: SourceClientOptions) => SanityClient - schema: Schema - i18n: LocaleSource - }, - documentIds: string[] = [], -): Observable> => { - return combineLatest( - documentIds.map((id) => { - const document$ = ctx.observeDocument(id).pipe( - distinctUntilChanged((prev, next) => { - if (prev?._rev === next?._rev) { - return true - } - // _rev and _updatedAt may change without other fields changing (due to a limitation in mutator) - // so only pass on documents if _other_ attributes changes - return shallowEquals(omit(prev, '_rev', '_updatedAt'), omit(next, '_rev', '_updatedAt')) - }), - shareLatestWithRefCount(), - ) - - return validateDocumentWithReferences(ctx, document$).pipe( - map((validation) => ({ - ...validation, - documentId: id, - hasError: validation.validation.some((marker) => isValidationErrorMarker(marker)), - })), - ) - }), - ).pipe( - // Transform to a dictionary with the id and value - scan((acc: Record, next) => { - const validations = {...acc} - next.forEach((status) => { - validations[status.documentId] = status - }) - return validations - }, {}), - shareLatestWithRefCount(), - ) -} diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentActions.tsx b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentActions.tsx index b70f27637e1b..7d1446ceb432 100644 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentActions.tsx +++ b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentActions.tsx @@ -24,7 +24,7 @@ export function DocumentActions({ const handleDiscardVersion = async () => { try { setDiscardStatus('discarding') - await client.delete(document._id) + await client.delete(document.document._id) setDiscardStatus('idle') } catch (e) { setDiscardStatus('error') @@ -70,7 +70,10 @@ export function DocumentActions({ }} > - + The {bundleTitle} version of this document will be permanently deleted. diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTableColumnDefs.tsx b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTableColumnDefs.tsx index 391d8cd9e926..e01294730a57 100644 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTableColumnDefs.tsx +++ b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTableColumnDefs.tsx @@ -14,21 +14,21 @@ export const getDocumentTableColumnDefs: ( id: 'search', width: null, header: (props) => , - cell: ({cellProps, datum: document}) => ( + cell: ({cellProps, datum: {document, previewValues, validation}}) => ( ), }, { - id: '_createdAt', + id: 'document._createdAt', sorting: true, width: 130, header: (props) => ( @@ -36,13 +36,11 @@ export const getDocumentTableColumnDefs: ( ), - cell: ({cellProps, datum: document}) => ( + cell: ({cellProps, datum: {document, history}}) => ( {document._createdAt && ( - {document.history?.createdBy && ( - - )} + {history?.createdBy && } @@ -52,7 +50,7 @@ export const getDocumentTableColumnDefs: ( ), }, { - id: '_updatedAt', + id: 'document._updatedAt', sorting: true, width: 130, header: (props) => ( @@ -60,13 +58,11 @@ export const getDocumentTableColumnDefs: ( ), - cell: ({cellProps, datum: document}) => ( + cell: ({cellProps, datum: {document, history}}) => ( {document._updatedAt && ( - {document.history?.lastEditedBy && ( - - )} + {history?.lastEditedBy && } @@ -76,7 +72,7 @@ export const getDocumentTableColumnDefs: ( ), }, { - id: '_publishedAt', + id: 'document._publishedAt', sorting: true, width: 130, header: (props) => ( @@ -84,7 +80,7 @@ export const getDocumentTableColumnDefs: ( ), - cell: ({cellProps, datum: document}) => ( + cell: ({cellProps, datum: {document}}) => ( {/* TODO: How to get the publishedAt date from the document, consider history API */} {/* {document._publishedAt && ( diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/useDocumentPreviewValues.ts b/packages/sanity/src/core/releases/tool/detail/documentTable/useDocumentPreviewValues.ts deleted file mode 100644 index 22d2f9fcb4a5..000000000000 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/useDocumentPreviewValues.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {type SanityDocument} from '@sanity/types' -import {useMemo} from 'react' -import {useObservable} from 'react-rx' -import { - getPreviewStateObservable, - getPreviewValueWithFallback, - prepareForPreview, - type SchemaType, - useDocumentPreviewStore, - useSchema, -} from 'sanity' - -import {type BundleDocument} from '../../../../store/bundles/types' - -export const useDocumentPreviewValues = ({ - release, - document, -}: { - release: BundleDocument - document: SanityDocument -}) => { - const schema = useSchema() - const schemaType = schema.get(document._type) as SchemaType | undefined - if (!schemaType) { - throw new Error(`Schema type "${document._type}" not found`) - } - const perspective = `bundle.${release.slug}` - - const documentPreviewStore = useDocumentPreviewStore() - - const previewStateObservable = useMemo( - () => - getPreviewStateObservable( - documentPreviewStore, - schemaType, - document._id, - 'Untitled', - perspective, - ), - [document._id, documentPreviewStore, perspective, schemaType], - ) - - const {draft, published, version, isLoading} = useObservable(previewStateObservable, { - draft: null, - isLoading: true, - published: null, - }) - - const previewValues = getPreviewValueWithFallback({ - value: document, - draft, - published, - version, - perspective, - }) - - return {previewValues: prepareForPreview(previewValues, schemaType), isLoading} -} diff --git a/packages/sanity/src/core/releases/tool/detail/review/DocumentDiffContainer.tsx b/packages/sanity/src/core/releases/tool/detail/review/DocumentDiffContainer.tsx index 3ec54e839103..985db71f3f93 100644 --- a/packages/sanity/src/core/releases/tool/detail/review/DocumentDiffContainer.tsx +++ b/packages/sanity/src/core/releases/tool/detail/review/DocumentDiffContainer.tsx @@ -1,4 +1,4 @@ -import {type ObjectSchemaType, type SanityDocument} from '@sanity/types' +import {type ObjectSchemaType} from '@sanity/types' import {Card, Flex} from '@sanity/ui' import {LoadingBlock} from '../../../../components/loadingBlock/LoadingBlock' @@ -6,61 +6,61 @@ import {useSchema} from '../../../../hooks/useSchema' import {useObserveDocument} from '../../../../preview/useObserveDocument' import {type BundleDocument} from '../../../../store/bundles/types' import {getPublishedId} from '../../../../util/draftUtils' -import {type DocumentValidationStatus} from '../bundleDocumentsValidation' -import {useDocumentPreviewValues} from '../documentTable/useDocumentPreviewValues' import {type DocumentHistory} from '../documentTable/useReleaseHistory' import {DocumentReviewHeader} from '../review/DocumentReviewHeader' +import {type DocumentInBundleResult} from '../useBundleDocuments' import {DocumentDiff} from './DocumentDiff' -export function DocumentDiffContainer({ - document, - release, - history, - searchTerm, - validation, -}: { - document: SanityDocument - release: BundleDocument - history?: DocumentHistory - searchTerm: string - validation?: DocumentValidationStatus -}) { +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, - schemaType, - ) - const {previewValues, isLoading} = useDocumentPreviewValues({document, release}) - if (searchTerm) { - // Early return to filter out documents that don't match the search term - const fallbackTitle = typeof document.title === 'string' ? document.title : 'Untitled' - const title = typeof previewValues.title === 'string' ? previewValues.title : fallbackTitle - if (!title.toLowerCase().includes(searchTerm.toLowerCase())) return null - } + const {document: baseDocument, loading: baseDocumentLoading} = useObserveDocument(publishedId) + if (baseDocumentLoading) return + + return +} + +export function DocumentDiffContainer({ + document, + history, + release, + previewValues, + validation, + isExpanded, + toggleIsExpanded, +}: { + document: DocumentInBundleResult['document'] + history?: DocumentHistory + release: BundleDocument + previewValues: DocumentInBundleResult['previewValues'] + validation?: DocumentInBundleResult['validation'] + isExpanded: boolean + toggleIsExpanded: () => void +}) { return ( - + - - {baseDocumentLoading ? ( - - ) : ( - - )} - + {isExpanded && ( + + + + )} ) } diff --git a/packages/sanity/src/core/releases/tool/detail/review/DocumentReviewHeader.tsx b/packages/sanity/src/core/releases/tool/detail/review/DocumentReviewHeader.tsx index 0ddab8aec81b..206920d21007 100644 --- a/packages/sanity/src/core/releases/tool/detail/review/DocumentReviewHeader.tsx +++ b/packages/sanity/src/core/releases/tool/detail/review/DocumentReviewHeader.tsx @@ -1,12 +1,14 @@ +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' import {Chip} from '../../../components/Chip' import {ReleaseDocumentPreview} from '../../../components/ReleaseDocumentPreview' -import {type DocumentValidationStatus} from '../bundleDocumentsValidation' +import {type DocumentValidationStatus} from '../useBundleDocuments' export function DocumentReviewHeader({ previewValues, @@ -15,12 +17,16 @@ 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 @@ -28,19 +34,26 @@ export function DocumentReviewHeader({ } }) { return ( - - - + + +