From 16532a8b725caaaa6ab873d9694d54529d395965 Mon Sep 17 00:00:00 2001 From: Pedro Bonamin <46196328+pedrobonamin@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:35:28 +0100 Subject: [PATCH] feat(structure): History UI updates (#7462) Merge draft changes into the publish action. Moves revision into the history panel Co-authored-by: RitaDias --- .../src/core/form/inputs/DateInputs/index.ts | 1 + .../src/core/form/inputs/DateInputs/utils.ts | 3 + .../sanity/src/core/i18n/bundles/studio.ts | 11 +- .../history/history/TimelineController.ts | 5 + .../store/_legacy/history/useTimelineStore.ts | 3 +- .../documentActions/HistoryRestoreAction.tsx | 6 +- .../documentActions/PublishAction.tsx | 9 +- .../sanity/src/structure/i18n/resources.ts | 32 ++- .../panes/document/DocumentPaneProvider.tsx | 2 +- .../document-layout/DocumentLayout.tsx | 6 +- .../header/DocumentPanelHeader.tsx | 13 +- .../inspectors/changes/ChangesInspector.tsx | 95 +++---- .../inspectors/changes/ChangesTabs.tsx | 88 +++++++ .../inspectors/changes/HistorySelector.tsx | 90 +++++++ .../document/inspectors/changes/index.ts | 8 +- .../document/statusBar/DocumentStatusBar.tsx | 63 +++-- .../document/statusBar/RevisionStatusLine.tsx | 54 ++++ .../__workshop__/TimelineItemStory.tsx | 200 ++++++++++++++ .../document/timeline/__workshop__/index.ts | 5 + .../panes/document/timeline/constants.ts | 6 +- .../timeline/expandableTimelineItemButton.tsx | 74 ++++++ .../document/timeline/timeline.styled.tsx | 5 +- .../panes/document/timeline/timeline.tsx | 141 ++++++++-- .../document/timeline/timelineItem.styled.tsx | 76 ------ .../panes/document/timeline/timelineItem.tsx | 241 +++++++++++------ .../panes/document/timeline/timelineMenu.tsx | 123 +++++---- .../document/timeline/userAvatarStack.tsx | 15 +- .../panes/document/timeline/utils.test.ts | 249 ++++++++++++++++++ .../panes/document/timeline/utils.ts | 81 ++++++ 29 files changed, 1343 insertions(+), 362 deletions(-) create mode 100644 packages/sanity/src/structure/panes/document/inspectors/changes/ChangesTabs.tsx create mode 100644 packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx create mode 100644 packages/sanity/src/structure/panes/document/statusBar/RevisionStatusLine.tsx create mode 100644 packages/sanity/src/structure/panes/document/timeline/__workshop__/TimelineItemStory.tsx create mode 100644 packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemButton.tsx delete mode 100644 packages/sanity/src/structure/panes/document/timeline/timelineItem.styled.tsx create mode 100644 packages/sanity/src/structure/panes/document/timeline/utils.test.ts create mode 100644 packages/sanity/src/structure/panes/document/timeline/utils.ts diff --git a/packages/sanity/src/core/form/inputs/DateInputs/index.ts b/packages/sanity/src/core/form/inputs/DateInputs/index.ts index f34d7d154b8..43635831208 100644 --- a/packages/sanity/src/core/form/inputs/DateInputs/index.ts +++ b/packages/sanity/src/core/form/inputs/DateInputs/index.ts @@ -1,2 +1,3 @@ export {DateInput, type DateInputProps} from './DateInput' export {DateTimeInput, type DateTimeInputProps} from './DateTimeInput' +export {getCalendarLabels} from './utils' diff --git a/packages/sanity/src/core/form/inputs/DateInputs/utils.ts b/packages/sanity/src/core/form/inputs/DateInputs/utils.ts index 82f7fca7309..e55c7987b41 100644 --- a/packages/sanity/src/core/form/inputs/DateInputs/utils.ts +++ b/packages/sanity/src/core/form/inputs/DateInputs/utils.ts @@ -4,6 +4,9 @@ export function isValidDate(date: Date): boolean { return date instanceof Date && !isNaN(date.valueOf()) } +/** + * @internal + */ export function getCalendarLabels( t: (key: string, values?: Record) => string, ): CalendarLabels { diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index e46949db912..8537b032b4f 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 */ @@ -359,6 +359,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', { 'document-status.not-published': 'Not published', /** Label to show in the document footer indicating the published date of the document */ 'document-status.published': 'Published {{date}}', + /** Label to show in the document footer indicating the revision from date of the document */ + 'document-status.revision-from': 'Revision from {{date}}', /** The value of the _key property must be a unique string. */ 'form.error.duplicate-keys-alert.details.additional-description': @@ -1592,7 +1594,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', { 'sheet-list.select-fields': 'Select up to 5 field types', /** Accessibility label for the navbar status button */ 'status-button.aria-label': 'Configuration status', - + /** Title for the changes tooltip in the history inspector*/ + 'timeline.changes.title': 'Changes by', /** Description for error when the timeline for the given document can't be loaded */ 'timeline.error.load-document-changes-description': 'Document history transactions have not been affected.', @@ -1656,7 +1659,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/core/store/_legacy/history/history/TimelineController.ts b/packages/sanity/src/core/store/_legacy/history/history/TimelineController.ts index 6b854286c1c..399377c3f3e 100644 --- a/packages/sanity/src/core/store/_legacy/history/history/TimelineController.ts +++ b/packages/sanity/src/core/store/_legacy/history/history/TimelineController.ts @@ -126,6 +126,10 @@ export class TimelineController { return this._revTime && typeof this._revTime === 'object' ? this._revTime : null } + get isLoading(): boolean { + return this._isRunning + } + get realRevChunk(): Chunk { return this.revTime || this.timeline.lastChunk() } @@ -258,6 +262,7 @@ export class TimelineController { !this._isSuspended if (!shouldFetchMore) { + this._isRunning = false return } diff --git a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts index 403a755fbb7..656886509d6 100644 --- a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts +++ b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts @@ -194,7 +194,6 @@ export function useTimelineStore({ findRangeForSince: (chunk: Chunk) => controller.findRangeForNewSince(chunk), loadMore: () => { controller.setLoadMore(true) - timelineStateRef.current.isLoading = true }, getSnapshot: () => timelineStateRef.current, subscribe: (callback: () => void) => { @@ -217,7 +216,7 @@ export function useTimelineStore({ return { chunks, diff: innerController.sinceTime ? innerController.currentObjectDiff() : null, - isLoading: false, + isLoading: innerController.isLoading, isPristine: timelineReady ? chunks.length === 0 && hasMoreChunks === false : null, hasMoreChunks: !innerController.timeline.reachedEarliestEntry, lastNonDeletedRevId: lastNonDeletedChunk?.[0]?.id, diff --git a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx index 7daf4f6848f..87fe61d468d 100644 --- a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx +++ b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx @@ -1,4 +1,4 @@ -import {RestoreIcon} from '@sanity/icons' +import {RevertIcon} from '@sanity/icons' import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import { type DocumentActionComponent, @@ -66,14 +66,14 @@ export const HistoryRestoreAction: DocumentActionComponent = ({id, type, revisio return { label: t('action.restore.label'), - color: 'primary', + tone: 'caution', onHandle: handle, title: t( isRevisionInitial ? 'action.restore.disabled.cannot-restore-initial' : 'action.restore.tooltip', ), - icon: RestoreIcon, + icon: RevertIcon, dialog, disabled: isRevisionInitial, } diff --git a/packages/sanity/src/structure/documentActions/PublishAction.tsx b/packages/sanity/src/structure/documentActions/PublishAction.tsx index 83df6415c5f..84ab9ec2e4f 100644 --- a/packages/sanity/src/structure/documentActions/PublishAction.tsx +++ b/packages/sanity/src/structure/documentActions/PublishAction.tsx @@ -111,19 +111,14 @@ export const PublishAction: DocumentActionComponent = (props) => { useEffect(() => { const didPublish = publishState === 'publishing' && !hasDraft - if (didPublish) { - if (changesOpen) { - // Re-open the panel - onHistoryOpen() - } - } + const nextState = didPublish ? 'published' : null const delay = didPublish ? 200 : 4000 const timer = setTimeout(() => { setPublishState(nextState) }, delay) return () => clearTimeout(timer) - }, [changesOpen, publishState, hasDraft, onHistoryOpen]) + }, [changesOpen, publishState, hasDraft]) const telemetry = useTelemetry() diff --git a/packages/sanity/src/structure/i18n/resources.ts b/packages/sanity/src/structure/i18n/resources.ts index 6bbfa4b3099..0ecd45bdb02 100644 --- a/packages/sanity/src/structure/i18n/resources.ts +++ b/packages/sanity/src/structure/i18n/resources.ts @@ -69,15 +69,15 @@ 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': 'Restore', + 'action.restore.label': 'Revert to revision', /** Default tooltip for the action */ - 'action.restore.tooltip': 'Restore to this version', + 'action.restore.tooltip': 'Restore to this revision', /** Tooltip when action is disabled because the document is not already published */ 'action.unpublish.disabled.not-published': 'This document is not published', @@ -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 */ @@ -467,6 +474,15 @@ const structureLocaleStrings = defineLocalesResources('structure', { 'structure-error.reload-button.text': 'Reload', /** Labels the structure path of the structure error screen */ 'structure-error.structure-path.label': 'Structure path', + + /** The aria label for the menu button in the timeline item */ + 'timeline-item.menu-button.aria-label': 'Open action menu', + /** The text for the tooltip in menu button the timeline item */ + 'timeline-item.menu-button.tooltip': 'Actions', + /** The text for the collapse action in the timeline item menu */ + 'timeline-item.menu.action-collapse': 'Collapse', + /** The text for the expand action in the timeline item menu */ + 'timeline-item.menu.action-expand': 'Expand', }) /** 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..50ed4c30e9c 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' @@ -57,7 +51,6 @@ export const DocumentPanelHeader = memo( onPaneSplit, menuItemGroups, schemaType, - timelineStore, connectionState, views, unstable_languageFilter, @@ -84,9 +77,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 +142,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..8eb38d8c7d6 100644 --- a/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx +++ b/packages/sanity/src/structure/panes/document/inspectors/changes/ChangesInspector.tsx @@ -1,27 +1,23 @@ import {type ObjectDiff} from '@sanity/diff' -import {AvatarStack, BoundaryElementProvider, Box, Card, Flex} from '@sanity/ui' -import {type ReactElement, useMemo, useRef} from 'react' +import {BoundaryElementProvider, Box, Card, Flex, Text} from '@sanity/ui' +import {type ReactElement, useMemo, useState} from 'react' import { ChangeFieldWrapper, ChangeList, - DiffTooltip, type DocumentChangeContextInstance, - type DocumentInspectorProps, LoadingBlock, NoChanges, type ObjectSchemaType, ScrollContainer, - UserAvatar, useTimelineSelector, useTranslation, } from 'sanity' 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' const Scroller = styled(ScrollContainer)` height: 100%; @@ -30,12 +26,20 @@ 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) + const [scrollRef, setScrollRef] = useState(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) @@ -45,7 +49,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( () => ({ @@ -60,52 +64,35 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { [documentId, diff, isComparingCurrent, schemaType, value], ) - const changeAnnotations = useMemo( - () => (diff ? collectLatestAuthorAnnotations(diff) : []), - [diff], - ) - return ( - - - - - + + + + {structureT('changes.from.label')} + - - - - {changeAnnotations.map(({author}) => ( - - ))} - - - - - + + + {structureT('changes.to.label')} + + + + - - - - - + + + + + {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'} + /> + + +