diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts
index ca7658410e7..cd142bf3669 100644
--- a/packages/sanity/src/core/i18n/bundles/studio.ts
+++ b/packages/sanity/src/core/i18n/bundles/studio.ts
@@ -197,7 +197,7 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'calendar.weekday-names.short.wednesday': 'Wed',
/** Label for the close button label in Review Changes pane */
- 'changes.action.close-label': 'Close review changes',
+ 'changes.action.close-label': 'Close history',
/** Cancel label for revert button prompt action */
'changes.action.revert-all-cancel': 'Cancel',
/** Revert all confirm label for revert button action - used on prompt button + review changes pane */
@@ -313,7 +313,7 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
/** Label for when the action of the change was a removal, eg a field was cleared, an array item was removed, an asset was deselected or similar */
'changes.removed-label': 'Removed',
/** Title for the Review Changes pane */
- 'changes.title': 'Review changes',
+ 'changes.title': 'History',
/** --- Common components --- */
/** Tooltip text for context menu buttons */
@@ -1658,7 +1658,7 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
* Label for determining since which version the changes for timeline menu dropdown are showing.
* Receives the time label as a parameter (`timestamp`).
*/
- 'timeline.since': 'Since: {{timestamp, datetime}}',
+ 'timeline.since': '{{timestamp, datetime}}',
/** Label for missing change version for timeline menu dropdown are showing */
'timeline.since-version-missing': 'Since: unknown version',
/** Aria label for the action buttons in the PTE toolbar */
diff --git a/packages/sanity/src/structure/i18n/resources.ts b/packages/sanity/src/structure/i18n/resources.ts
index f87408561d6..933162b1d4f 100644
--- a/packages/sanity/src/structure/i18n/resources.ts
+++ b/packages/sanity/src/structure/i18n/resources.ts
@@ -69,10 +69,10 @@ const structureLocaleStrings = defineLocalesResources('structure', {
/** Tooltip when publish button is waiting for validation and async tasks to complete.*/
'action.publish.waiting': 'Waiting for tasks to finish before publishing',
- /** Message prompting the user to confirm that they want to restore to an earlier version*/
+ /** Message prompting the user to confirm that they want to restore to an earlier revision*/
'action.restore.confirm.message': 'Are you sure you want to restore this document?',
- /** Fallback tooltip for when user is looking at the initial version */
- 'action.restore.disabled.cannot-restore-initial': "You can't restore to the initial version",
+ /** Fallback tooltip for when user is looking at the initial revision */
+ 'action.restore.disabled.cannot-restore-initial': "You can't restore to the initial revision",
/** Label for the "Restore" document action */
'action.restore.label': 'Revert to revision',
@@ -90,7 +90,7 @@ const structureLocaleStrings = defineLocalesResources('structure', {
'This document has live edit enabled and cannot be unpublished',
/** The text for the restore button on the deleted document banner */
- 'banners.deleted-document-banner.restore-button.text': 'Restore most recent version',
+ 'banners.deleted-document-banner.restore-button.text': 'Restore most recent revision',
/** The text content for the deleted document banner */
'banners.deleted-document-banner.text': 'This document has been deleted.',
/** The text content for the deprecated document type banner */
@@ -147,7 +147,14 @@ const structureLocaleStrings = defineLocalesResources('structure', {
'buttons.split-pane-close-button.title': 'Close split pane',
/** The title for the close group button on the split pane on the document panel header */
'buttons.split-pane-close-group-button.title': 'Close pane group',
-
+ /** The label used in the changes inspector for the from selector */
+ 'changes.from.label': 'From',
+ /* The label for the history tab in the changes inspector*/
+ 'changes.tab.history': 'History',
+ /* The label for the review tab in the changes inspector*/
+ 'changes.tab.review-changes': 'Review changes',
+ /** The label used in the changes inspector for the to selector */
+ 'changes.to.label': 'To',
/** The text in the "Cancel" button in the confirm delete dialog that cancels the action and closes the dialog */
'confirm-delete-dialog.cancel-button.text': 'Cancel',
/** Used in `confirm-delete-dialog.cdr-summary.title` */
@@ -378,7 +385,7 @@ const structureLocaleStrings = defineLocalesResources('structure', {
'{{title}} was restored',
/** The text when an unpublish operation succeeded */
'panes.document-operation-results.operation-success_unpublish':
- '{{title}} was unpublished. A draft has been created from the latest published version.',
+ '{{title}} was unpublished. A draft has been created from the latest published revision.',
/** The document title shown when document title is "undefined" in operation message */
'panes.document-operation-results.operation-undefined-title': 'Untitled',
/** The title of the reconnecting toast */
diff --git a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
index 4aeafcf9835..3e7a4510056 100644
--- a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
+++ b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
@@ -390,7 +390,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
}
if (resolvedChangesInspector) {
- openInspector(resolvedChangesInspector.name)
+ openInspector(resolvedChangesInspector.name, {changesInspectorTab: 'review'})
}
}, [features.reviewChanges, openInspector, resolvedChangesInspector])
diff --git a/packages/sanity/src/structure/panes/document/document-layout/DocumentLayout.tsx b/packages/sanity/src/structure/panes/document/document-layout/DocumentLayout.tsx
index a1c8a917103..e9068373060 100644
--- a/packages/sanity/src/structure/panes/document/document-layout/DocumentLayout.tsx
+++ b/packages/sanity/src/structure/panes/document/document-layout/DocumentLayout.tsx
@@ -25,7 +25,7 @@ import {type Path} from 'sanity-diff-patch'
import {styled} from 'styled-components'
import {TooltipDelayGroupProvider} from '../../../../ui-components'
-import {Pane, PaneFooter, usePane, usePaneLayout} from '../../../components'
+import {Pane, PaneFooter, usePane, usePaneLayout, usePaneRouter} from '../../../components'
import {DOCUMENT_PANEL_PORTAL_ELEMENT} from '../../../constants'
import {structureLocaleNamespace} from '../../../i18n'
import {useStructureTool} from '../../../useStructureTool'
@@ -82,7 +82,7 @@ export function DocumentLayout() {
schemaType,
value,
} = useDocumentPane()
-
+ const {params: paneParams} = usePaneRouter()
const {features} = useStructureTool()
const {t} = useTranslation(structureLocaleNamespace)
const {collapsed: layoutCollapsed} = usePaneLayout()
@@ -228,7 +228,7 @@ export function DocumentLayout() {
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx
index ef37d951f93..a2acd8d91d8 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx
@@ -10,12 +10,7 @@ import {
useMemo,
useState,
} from 'react'
-import {
- type DocumentActionDescription,
- useFieldActions,
- useTimelineSelector,
- useTranslation,
-} from 'sanity'
+import {type DocumentActionDescription, useFieldActions, useTranslation} from 'sanity'
import {Button, TooltipDelayGroupProvider} from '../../../../../ui-components'
import {
@@ -33,7 +28,6 @@ import {type PaneMenuItem} from '../../../../types'
import {useStructureTool} from '../../../../useStructureTool'
import {ActionDialogWrapper, ActionMenuListItem} from '../../statusBar/ActionMenuButton'
import {isRestoreAction} from '../../statusBar/DocumentStatusBarActions'
-import {TimelineMenu} from '../../timeline'
import {useDocumentPane} from '../../useDocumentPane'
import {DocumentHeaderTabs} from './DocumentHeaderTabs'
import {DocumentHeaderTitle} from './DocumentHeaderTitle'
@@ -84,9 +78,6 @@ export const DocumentPanelHeader = memo(
const contextMenuNodes = useMemo(() => menuNodes.filter(isNotMenuNodeButton), [menuNodes])
const showTabs = views.length > 1
- // Subscribe to external timeline state changes
- const rev = useTimelineSelector(timelineStore, (state) => state.revTime)
-
const {collapsed, isLast} = usePane()
// Prevent focus if this is the last (non-collapsed) pane.
const tabIndex = isLast && !collapsed ? -1 : 0
@@ -152,7 +143,6 @@ export const DocumentPanelHeader = memo(
/>
)
}
- subActions={}
actions={
{unstable_languageFilter.length > 0 && (
diff --git a/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx b/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx
index 6b64cde3a3b..2da42653a8c 100644
--- a/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx
+++ b/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx
@@ -1,12 +1,11 @@
import {type ObjectDiff} from '@sanity/diff'
-import {AvatarStack, BoundaryElementProvider, Box, Card, Flex} from '@sanity/ui'
+import {AvatarStack, BoundaryElementProvider, Box, Card, Flex, Text} from '@sanity/ui'
import {type ReactElement, useMemo, useRef} from 'react'
import {
ChangeFieldWrapper,
ChangeList,
DiffTooltip,
type DocumentChangeContextInstance,
- type DocumentInspectorProps,
LoadingBlock,
NoChanges,
type ObjectSchemaType,
@@ -18,7 +17,7 @@ import {
import {DocumentChangeContext} from 'sanity/_singletons'
import {styled} from 'styled-components'
-import {DocumentInspectorHeader} from '../../documentInspector'
+import {structureLocaleNamespace} from '../../../../i18n'
import {TimelineMenu} from '../../timeline'
import {useDocumentPane} from '../../useDocumentPane'
import {collectLatestAuthorAnnotations} from './helpers'
@@ -30,12 +29,21 @@ const Scroller = styled(ScrollContainer)`
scroll-behavior: smooth;
`
-export function ChangesInspector(props: DocumentInspectorProps): ReactElement {
- const {onClose} = props
+const Grid = styled(Box)`
+ &:not([hidden]) {
+ display: grid;
+ }
+ grid-template-columns: 48px 1fr;
+ align-items: center;
+ gap: 0.25em;
+`
+
+export function ChangesInspector({showChanges}: {showChanges: boolean}): ReactElement {
const {documentId, schemaType, timelineError, timelineStore, value} = useDocumentPane()
const scrollRef = useRef(null)
// Subscribe to external timeline state changes
+ const rev = useTimelineSelector(timelineStore, (state) => state.revTime)
const diff = useTimelineSelector(timelineStore, (state) => state.diff)
const onOlderRevision = useTimelineSelector(timelineStore, (state) => state.onOlderRevision)
const selectionState = useTimelineSelector(timelineStore, (state) => state.selectionState)
@@ -46,6 +54,7 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement {
// Note that we are using the studio core namespace here, as changes theoretically should
// be part of Sanity core (needs to be moved from structure at some point)
const {t} = useTranslation('studio')
+ const {t: structureT} = useTranslation(structureLocaleNamespace)
const documentContext: DocumentChangeContextInstance = useMemo(
() => ({
@@ -67,19 +76,20 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement {
return (
-
-
-
-
-
+
+
+
+ {structureT('changes.from.label')}
+
-
+
+
+ {structureT('changes.to.label')}
+
+
+
+ {changeAnnotations.length > 0 && (
+
{changeAnnotations.map(({author}) => (
-
+
))}
-
-
-
+
+ )}
+
-
-
+
+ {showChanges && (
+
+ )}
diff --git a/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesTabs.tsx b/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesTabs.tsx
new file mode 100644
index 00000000000..8fe53fd88d7
--- /dev/null
+++ b/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesTabs.tsx
@@ -0,0 +1,88 @@
+import {CloseIcon} from '@sanity/icons'
+import {Box, Flex, TabList, TabPanel} from '@sanity/ui'
+import {type DocumentInspectorProps, useTranslation} from 'sanity'
+import {styled} from 'styled-components'
+
+import {Button, Tab} from '../../../../../ui-components'
+import {usePaneRouter} from '../../../../components/paneRouter/usePaneRouter'
+import {structureLocaleNamespace} from '../../../../i18n'
+import {HISTORY_INSPECTOR_NAME} from '../../constants'
+import {ChangesInspector} from './ChangesInspector'
+import {HistorySelector} from './HistorySelector'
+
+const FadeInFlex = styled(Flex)`
+ opacity: 0;
+ transition: opacity 200ms;
+ &[data-ready] {
+ opacity: 1;
+ }
+`
+const TABS = ['history', 'review'] as const
+const isValidTab = (tab: string | undefined): tab is (typeof TABS)[number] =>
+ // @ts-expect-error TS doesn't understand the type guard
+ tab && TABS.includes(tab)
+
+export function ChangesTabs(props: DocumentInspectorProps) {
+ const {params, setParams} = usePaneRouter()
+ const {t} = useTranslation(structureLocaleNamespace)
+ const isReady = params?.inspect === HISTORY_INSPECTOR_NAME
+
+ const paneRouterTab = isValidTab(params?.changesInspectorTab)
+ ? params.changesInspectorTab
+ : TABS[0]
+ const setPaneRouterTab = (tab: (typeof TABS)[number]) =>
+ setParams({
+ ...params,
+ changesInspectorTab: tab,
+ })
+
+ return (
+
+
+
+ setPaneRouterTab('history')}
+ selected={paneRouterTab === 'history'}
+ />
+ setPaneRouterTab('review')}
+ selected={paneRouterTab === 'review'}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx b/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx
new file mode 100644
index 00000000000..62984f06054
--- /dev/null
+++ b/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx
@@ -0,0 +1,91 @@
+import {BoundaryElementProvider, Card, Flex, useToast} from '@sanity/ui'
+import {useCallback, useRef, useState} from 'react'
+import {type Chunk, ScrollContainer, useTimelineSelector, useTranslation} from 'sanity'
+import {styled} from 'styled-components'
+
+import {Timeline} from '../../timeline'
+import {TimelineError} from '../../timeline/TimelineError'
+import {useDocumentPane} from '../../useDocumentPane'
+
+const Scroller = styled(ScrollContainer)`
+ height: 100%;
+ overflow: auto;
+ position: relative;
+ scroll-behavior: smooth;
+`
+
+export function HistorySelector({showList}: {showList: boolean}) {
+ const {timelineError, setTimelineMode, setTimelineRange, timelineStore} = useDocumentPane()
+ const scrollRef = useRef(null)
+ const [listHeight, setListHeight] = useState(0)
+
+ const getScrollerRef = useCallback((el: HTMLDivElement | null) => {
+ /**
+ * Hacky solution, the list height needs to be defined, it cannot be obtained from the parent using a `max-height: 100%`
+ * Because the scroller won't work properly and it won't scroll to the selected element on mount.
+ * To fix this, this component will set the list height to the height of the parent element - 1px, to avoid a double scroll line.
+ */
+ setListHeight(el?.clientHeight ? el.clientHeight - 1 : 0)
+ scrollRef.current = el
+ }, [])
+
+ const chunks = useTimelineSelector(timelineStore, (state) => state.chunks)
+ const realRevChunk = useTimelineSelector(timelineStore, (state) => state.realRevChunk)
+ const hasMoreChunks = useTimelineSelector(timelineStore, (state) => state.hasMoreChunks)
+ const loading = useTimelineSelector(timelineStore, (state) => state.isLoading)
+
+ const {t} = useTranslation('studio')
+ const toast = useToast()
+ const selectRev = useCallback(
+ (revChunk: Chunk) => {
+ try {
+ const [sinceId, revId] = timelineStore.findRangeForRev(revChunk)
+ setTimelineMode('closed')
+ setTimelineRange(sinceId, revId)
+ } catch (err) {
+ toast.push({
+ closable: true,
+ description: err.message,
+ status: 'error',
+ title: t('timeline.error.unable-to-load-revision'),
+ })
+ }
+ },
+ [setTimelineMode, setTimelineRange, t, timelineStore, toast],
+ )
+
+ const handleLoadMore = useCallback(() => {
+ // If updated, be sure to update the TimeLineMenu component as well
+ if (!loading) {
+ timelineStore.loadMore()
+ }
+ }, [loading, timelineStore])
+
+ return (
+
+
+ {timelineError ? (
+
+ ) : (
+
+
+ {listHeight &&
+ // This forces the list to unmount and remount, which is needed to reset the scroll position
+ showList ? (
+
+ ) : null}
+
+
+ )}
+
+
+ )
+}
diff --git a/packages/sanity/src/structure/panes/document/inspectors/changes/index.ts b/packages/sanity/src/structure/panes/document/inspectors/changes/index.ts
index f0babd84a2d..4b93f59ee6d 100644
--- a/packages/sanity/src/structure/panes/document/inspectors/changes/index.ts
+++ b/packages/sanity/src/structure/panes/document/inspectors/changes/index.ts
@@ -3,7 +3,7 @@ import {type DocumentInspector, useTranslation} from 'sanity'
import {useStructureTool} from '../../../../useStructureTool'
import {HISTORY_INSPECTOR_NAME} from '../../constants'
-import {ChangesInspector} from './ChangesInspector'
+import {ChangesTabs} from './ChangesTabs'
export const changesInspector: DocumentInspector = {
name: HISTORY_INSPECTOR_NAME,
@@ -17,7 +17,7 @@ export const changesInspector: DocumentInspector = {
title: t('changes.title'),
}
},
- component: ChangesInspector,
- onClose: ({params}) => ({params: {...params, since: undefined}}),
+ component: ChangesTabs,
+ onClose: ({params}) => ({params: {...params, since: undefined, changesInspectorTab: undefined}}),
onOpen: ({params}) => ({params: {...params, since: '@lastPublished'}}),
}
diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.styled.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.styled.tsx
index e2f5723ee42..9fda8a65c73 100644
--- a/packages/sanity/src/structure/panes/document/timeline/timeline.styled.tsx
+++ b/packages/sanity/src/structure/panes/document/timeline/timeline.styled.tsx
@@ -5,8 +5,8 @@ export const StackWrapper = styled(Stack)`
max-width: 200px;
`
-export const ListWrapper = styled(Flex)`
- max-height: calc(100vh - 198px);
+export const ListWrapper = styled(Flex)<{$maxHeight: string}>`
+ max-height: ${(props) => props.$maxHeight};
min-width: 244px;
`
@@ -14,6 +14,7 @@ export const Root = styled(Box)<{$visible?: boolean}>(({$visible}) => {
return css`
opacity: 0;
pointer-events: none;
+ transition: opacity 0.2s;
${$visible &&
css`
diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx
index 8fd2d0f03ac..5e3296f318f 100644
--- a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx
+++ b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx
@@ -19,6 +19,10 @@ interface TimelineProps {
lastChunk?: Chunk | null
onLoadMore: () => void
onSelect: (chunk: Chunk) => void
+ /**
+ * The list needs a predefined max height for the scroller to work.
+ */
+ listMaxHeight?: string
}
export const Timeline = ({
@@ -29,6 +33,7 @@ export const Timeline = ({
onLoadMore,
onSelect,
firstChunk,
+ listMaxHeight = 'calc(100vh - 198px)',
}: TimelineProps) => {
const [mounted, setMounted] = useState(false)
const {t} = useTranslation('studio')
@@ -97,7 +102,7 @@ export const Timeline = ({
)}
{filteredChunks.length > 0 && (
-
+
+ width="fill"
+ tooltipProps={null}
+ >
+
+
+ {ready ? buttonLabel : t('timeline.loading-history')}
+
+
+
+
+
+
)
}