diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx index bbd447ada65..4219f338293 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx @@ -11,6 +11,7 @@ import {type BundleDocument} from '../../../store/bundles/types' import {API_VERSION} from '../../../tasks/constants' import {BundleMenuButton} from '../../components/BundleMenuButton/BundleMenuButton' import {type ReleasesRouterState} from '../../types/router' +import {useReleaseHistory} from './documentTable/useReleaseHistory' import {ReleaseOverview} from './ReleaseOverview' type Screen = 'overview' | 'review' @@ -29,6 +30,8 @@ export const ReleaseDetail = () => { const {data, loading} = useBundles() const {documents: bundleDocuments, loading: documentsLoading} = useFetchBundleDocuments(parsedBundleName) + const history = useReleaseHistory(bundleDocuments) + const bundle = data?.find((storeBundle) => storeBundle.name === parsedBundleName) const bundleHasDocuments = !!bundleDocuments.length const showPublishButton = loading || !bundle?.publishedAt @@ -120,7 +123,12 @@ export const ReleaseDetail = () => { ) : ( <> {activeScreen === 'overview' && ( - + )} )} diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseOverview.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseOverview.tsx index 68bfc82deca..a52db886094 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseOverview.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseOverview.tsx @@ -13,14 +13,17 @@ import {type BundleDocument} from '../../../store/bundles/types' import {useAddonDataset} from '../../../studio/addonDataset/useAddonDataset' import {Chip} from '../../components/Chip' import {DocumentTable} from './documentTable' +import {type DocumentHistory} from './documentTable/useReleaseHistory' -export function ReleaseOverview(props: {documents: SanityDocument[]; release: BundleDocument}) { - const {documents, release} = props +export function ReleaseOverview(props: { + documents: SanityDocument[] + documentsHistory: Map + collaborators: string[] + release: BundleDocument +}) { + const {documents, documentsHistory, release, collaborators} = props const {client} = useAddonDataset() - /** - * This state is created here but will be updated by the DocumentRow component when fetching the history - */ - const [collaborators, setCollaborators] = useState([]) + const [iconValue, setIconValue] = useState({ hue: release.hue ?? 'gray', icon: release.icon ?? 'documents', @@ -132,7 +135,7 @@ export function ReleaseOverview(props: {documents: SanityDocument[]; release: Bu )} diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentRow.tsx b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentRow.tsx index 133b9af28f7..a5444a2d03e 100644 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentRow.tsx +++ b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentRow.tsx @@ -1,14 +1,7 @@ import {CheckmarkCircleIcon, EmptyIcon, Progress50Icon} from '@sanity/icons' import {type SanityDocument} from '@sanity/types' import {AvatarStack, Box, Card, Flex, Text} from '@sanity/ui' -import { - type Dispatch, - type ForwardedRef, - forwardRef, - type SetStateAction, - useEffect, - useMemo, -} from 'react' +import {type ForwardedRef, forwardRef, useMemo} from 'react' import {getPublishedId, RelativeTime, SanityDefaultPreview, UserAvatar} from 'sanity' import {IntentLink} from 'sanity/router' @@ -16,7 +9,7 @@ import {Tooltip} from '../../../../../ui-components' import {type BundleDocument} from '../../../../store/bundles/types' import {DocumentActions} from './DocumentActions' import {useDocumentPreviewValues} from './useDocumentPreviewValues' -import {useVersionHistory} from './useVersionHistory' +import {type DocumentHistory} from './useReleaseHistory' const DOCUMENT_STATUS = { ready: { @@ -63,19 +56,22 @@ export function DocumentRow(props: { searchTerm: string document: SanityDocument release: BundleDocument - setCollaborators: Dispatch> + history: DocumentHistory | undefined }) { - const {document, release, searchTerm, setCollaborators} = props + const { + document, + release, + searchTerm, + history = { + editors: [], + createdBy: undefined, + lastEditedBy: undefined, + }, + } = props const documentId = document._id const documentTypeName = document._type const {previewValues, isLoading} = useDocumentPreviewValues({document, release}) - const history = useVersionHistory(documentId, document?._rev) - - useEffect(() => { - setCollaborators((pre) => Array.from(new Set([...pre, ...history.editors]))) - }, [history.editors, setCollaborators]) - const LinkComponent = useMemo( () => // eslint-disable-next-line @typescript-eslint/no-shadow diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTable.tsx b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTable.tsx index 7399ab9c674..8ae612b1673 100644 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTable.tsx +++ b/packages/sanity/src/core/releases/tool/detail/documentTable/DocumentTable.tsx @@ -1,12 +1,13 @@ import {type SanityDocument} from '@sanity/types' import {Stack} from '@sanity/ui' -import {type Dispatch, type SetStateAction, useMemo, useState} from 'react' +import {useMemo, useState} from 'react' import {styled} from 'styled-components' import {type BundleDocument} from '../../../../store/bundles/types' import {DocumentHeader} from './DocumentHeader' import {DocumentRow} from './DocumentRow' import {type DocumentSort} from './types' +import {type DocumentHistory} from './useReleaseHistory' const RowStack = styled(Stack)({ '& > *:not(:first-child)': { @@ -23,10 +24,10 @@ const RowStack = styled(Stack)({ export function DocumentTable(props: { documents: SanityDocument[] + documentsHistory: Map release: BundleDocument - setCollaborators: Dispatch> }) { - const {documents, release, setCollaborators} = props + const {documents, release, documentsHistory} = props // Filter will happen at the DocumentRow level because we don't have access here to the preview values. const [searchTerm, setSearchTerm] = useState('') const [sort, setSort] = useState({property: '_updatedAt', order: 'desc'}) @@ -66,7 +67,7 @@ export function DocumentTable(props: { document={d} key={d._id} release={release} - setCollaborators={setCollaborators} + history={documentsHistory.get(d._id)} /> ))} diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/useReleaseHistory.ts b/packages/sanity/src/core/releases/tool/detail/documentTable/useReleaseHistory.ts new file mode 100644 index 00000000000..485aa23207c --- /dev/null +++ b/packages/sanity/src/core/releases/tool/detail/documentTable/useReleaseHistory.ts @@ -0,0 +1,98 @@ +import {useCallback, useEffect, useMemo, useState} from 'react' +import { + type BundleDocument, + getPublishedId, + type TransactionLogEventWithEffects, + useClient, +} from 'sanity' + +import {getJsonStream} from '../../../../store/_legacy/history/history/getJsonStream' +import {API_VERSION} from '../../../../tasks/constants' + +export type DocumentHistory = { + history: TransactionLogEventWithEffects[] + createdBy: string + lastEditedBy: string + editors: string[] +} + +// TODO: Update this to contemplate the _revision change on any of the internal bundle documents, and fetch only the history of that document if changes. +export function useReleaseHistory(bundleDocuments: BundleDocument[]): { + documentsHistory: Map + collaborators: string[] + loading: boolean +} { + const client = useClient({apiVersion: API_VERSION}) + const {dataset, token} = client.config() + const [history, setHistory] = useState([]) + const queryParams = `tag=sanity.studio.tasks.history&effectFormat=mendoza&excludeContent=true&includeIdentifiedDocumentsOnly=true` + const bundleDocumentsIds = useMemo(() => bundleDocuments.map((doc) => doc._id), [bundleDocuments]) + + const publishedIds = bundleDocumentsIds.map((id) => getPublishedId(id)).join(',') + const transactionsUrl = client.getUrl( + `/data/history/${dataset}/transactions/${publishedIds}?${queryParams}`, + ) + + const fetchAndParseAll = useCallback(async () => { + if (!publishedIds) return + const transactions: TransactionLogEventWithEffects[] = [] + const stream = await getJsonStream(transactionsUrl, token) + const reader = stream.getReader() + let result + for (;;) { + result = await reader.read() + if (result.done) { + break + } + if ('error' in result.value) { + throw new Error(result.value.error.description || result.value.error.type) + } + transactions.push(result.value) + } + setHistory(transactions) + }, [publishedIds, transactionsUrl, token]) + + useEffect(() => { + fetchAndParseAll() + // When revision changes, update the history. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetchAndParseAll]) + + return useMemo(() => { + const collaborators: string[] = [] + const documentsHistory = new Map() + if (!history.length) { + return {documentsHistory, collaborators, loading: true} + } + history.forEach((item) => { + const documentId = item.documentIDs[0] + let documentHistory = documentsHistory.get(documentId) + if (!collaborators.includes(item.author)) { + collaborators.push(item.author) + } + // eslint-disable-next-line no-negated-condition + if (!documentHistory) { + documentHistory = { + history: [item], + createdBy: item.author, + lastEditedBy: item.author, + editors: [item.author], + } + documentsHistory.set(documentId, documentHistory) + } else { + // @ts-expect-error TransactionLogEventWithEffects has no property 'mutations' but it's returned from the API + const isCreate = item.mutations.some((mutation) => 'create' in mutation) + if (isCreate) documentHistory.createdBy = item.author + if (!documentHistory.editors.includes(item.author)) { + documentHistory.editors.push(item.author) + } + // The last item in the history is the last edited by, transaction log is ordered by timestamp + documentHistory.lastEditedBy = item.author + // always add history item + documentHistory.history.push(item) + } + }) + + return {documentsHistory, collaborators, loading: false} + }, [history]) +} diff --git a/packages/sanity/src/core/releases/tool/detail/documentTable/useVersionHistory.ts b/packages/sanity/src/core/releases/tool/detail/documentTable/useVersionHistory.ts deleted file mode 100644 index 2f701b82d1d..00000000000 --- a/packages/sanity/src/core/releases/tool/detail/documentTable/useVersionHistory.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {useCallback, useEffect, useMemo, useState} from 'react' -import {getPublishedId, type TransactionLogEventWithEffects, useClient} from 'sanity' - -import {getJsonStream} from '../../../../store/_legacy/history/history/getJsonStream' -import {API_VERSION} from '../../../../tasks/constants' - -/** - * TODO: - * Temporal solution, will be replaced once we have the API endpoint that returns all the necessary data. - */ -export function useVersionHistory(id: string, revision: string) { - const client = useClient({apiVersion: API_VERSION}) - const {dataset, token} = client.config() - const [history, setHistory] = useState([]) - const queryParams = `tag=sanity.studio.tasks.history&effectFormat=mendoza&excludeContent=true&includeIdentifiedDocumentsOnly=true` - const publishedId = getPublishedId(id) - const transactionsUrl = client.getUrl( - `/data/history/${dataset}/transactions/${publishedId}?${queryParams}`, - ) - - const fetchAndParse = useCallback(async () => { - const transactions: TransactionLogEventWithEffects[] = [] - const stream = await getJsonStream(transactionsUrl, token) - const reader = stream.getReader() - let result - for (;;) { - result = await reader.read() - if (result.done) { - break - } - if ('error' in result.value) { - throw new Error(result.value.error.description || result.value.error.type) - } - transactions.push(result.value) - setHistory(transactions) - } - }, [transactionsUrl, token]) - - useEffect(() => { - fetchAndParse() - // When revision changes, update the history. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetchAndParse, revision]) - const createdBy = history[0]?.author - const lastEditedBy = history[history.length - 1]?.author - const editors = useMemo( - () => Array.from(new Set(history.map((event) => event.author).filter(Boolean))), - [history], - ) - - return {history, createdBy, lastEditedBy, editors} -}