From 0171cd6e2fe93c232b033d05fc3c3444dc2d3e6a Mon Sep 17 00:00:00 2001 From: pedrobonamin Date: Mon, 2 Sep 2024 17:58:37 +0200 Subject: [PATCH 1/4] feat(structure): add squashed chunks to timeline --- .../sanity/src/structure/i18n/resources.ts | 2 + .../__workshop__/TimelineItemStory.tsx | 127 +++++----- .../timeline/expandableTimelineItem.tsx | 133 ++++++++++ .../panes/document/timeline/timeline.tsx | 73 ++++-- .../panes/document/timeline/timelineItem.tsx | 45 +--- .../panes/document/timeline/utils.test.ts | 230 ++++++++++++++++++ .../panes/document/timeline/utils.ts | 44 ++++ 7 files changed, 533 insertions(+), 121 deletions(-) create mode 100644 packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.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/structure/i18n/resources.ts b/packages/sanity/src/structure/i18n/resources.ts index 6e6ee30ec25..c44405de6ad 100644 --- a/packages/sanity/src/structure/i18n/resources.ts +++ b/packages/sanity/src/structure/i18n/resources.ts @@ -452,6 +452,8 @@ const structureLocaleStrings = defineLocalesResources('structure', { '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/timeline/__workshop__/TimelineItemStory.tsx b/packages/sanity/src/structure/panes/document/timeline/__workshop__/TimelineItemStory.tsx index 890deda5acb..c930da2cac9 100644 --- a/packages/sanity/src/structure/panes/document/timeline/__workshop__/TimelineItemStory.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/__workshop__/TimelineItemStory.tsx @@ -1,13 +1,34 @@ import {Box, Card, Container, Stack, Text} from '@sanity/ui' -import {useMemo, useState} from 'react' -import {type ChunkType, getCalendarLabels, useDateTimeFormat, useTranslation} from 'sanity' +import {useCallback, useMemo, useState} from 'react' +import { + type Chunk, + type ChunkType, + getCalendarLabels, + useDateTimeFormat, + useTranslation, +} from 'sanity' import {DateTimeInput} from '../../../../../ui-components/inputs/DateInputs/DateTimeInput' +import {ExpandableTimelineItem} from '../expandableTimelineItem' import {TIMELINE_ITEM_I18N_KEY_MAPPING} from '../timelineI18n' import {TimelineItem} from '../timelineItem' const CHUNK_TYPES = Object.keys(TIMELINE_ITEM_I18N_KEY_MAPPING).reverse() as ChunkType[] +function createChunk(type: ChunkType, index: number, date: Date): Chunk { + return { + index, + id: type, + type: type, + start: -13, + end: -13, + startTimestamp: date.toString(), + endTimestamp: date.toString(), + authors: new Set(['p8xDvUMxC']), + draftState: 'unknown', + publishedState: 'present', + } +} export default function TimelineItemStory() { const {t: coreT} = useTranslation() const [date, setDate] = useState(() => new Date()) @@ -24,6 +45,10 @@ export default function TimelineItemStory() { } } + const handleSelect = useCallback((chunk: Chunk) => { + setSelected((c) => (c === chunk.id ? null : chunk.id)) + }, []) + return ( @@ -52,69 +77,41 @@ export default function TimelineItemStory() { - {CHUNK_TYPES.map((key, index) => ( - setSelected((p) => (p === key ? null : key))} - isSelected={selected === key} - type={key} - timestamp={date.toString()} - chunk={{ - index, - id: key, - type: key, - start: -13, - end: -13, - startTimestamp: date.toString(), - endTimestamp: date.toString(), - authors: new Set(['p8xDvUMxC']), - draftState: 'unknown', - publishedState: 'present', - }} - squashedChunks={ - key === 'publish' - ? [ - { - index: 0, - id: '123', - type: 'editDraft', - start: 0, - end: 0, - startTimestamp: date.toString(), - endTimestamp: date.toString(), - authors: new Set(['pP5s3g90N']), - draftState: 'present', - publishedState: 'present', - }, - { - index: 1, - id: '345', - type: 'editDraft', - start: 1, - end: 1, - startTimestamp: date.toString(), - endTimestamp: date.toString(), - authors: new Set(['pJ61yWhkD']), - draftState: 'present', - publishedState: 'present', - }, - { - index: 2, - id: '345', - type: 'editDraft', - start: 2, - end: 2, - startTimestamp: date.toString(), - endTimestamp: date.toString(), - authors: new Set(['pJ61yWhkD']), - draftState: 'present', - publishedState: 'present', - }, - ] - : undefined - } - /> - ))} + {CHUNK_TYPES.map((type, index) => { + if (type === 'publish') { + return ( + + ) + } + return ( + + ) + })} diff --git a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx new file mode 100644 index 00000000000..35763cccf20 --- /dev/null +++ b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx @@ -0,0 +1,133 @@ +import {Box, Menu} from '@sanity/ui' +import {type MouseEvent, useCallback, useMemo, useState} from 'react' +import {type Chunk, ContextMenuButton, useTranslation} from 'sanity' +import {styled} from 'styled-components' + +import {MenuButton, MenuItem} from '../../../../ui-components' +import {structureLocaleNamespace} from '../../../i18n' +import {TimelineItem} from './timelineItem' + +const ExpandedWrapper = styled.div` + display: grid; + grid-template-columns: 1fr; + grid-auto-rows: 0fr; + overflow: hidden; + transition: all 200ms ease; + padding-left: 16px; + &[data-expanded] { + grid-auto-rows: 1fr; + padding-top: 4px; + padding-bottom: 4px; + gap: 4px; + } +` + +function TimelineItemMenu({ + chunkId, + isExpanded, + onExpandClick, +}: { + chunkId: string + isExpanded: boolean + onExpandClick: () => void +}) { + const {t} = useTranslation(structureLocaleNamespace) + const handleExpandClick = useCallback( + (e: MouseEvent) => { + // Avoid the click event to propagate to the parent button and closing the popover + e.stopPropagation() + onExpandClick() + }, + [onExpandClick], + ) + return ( + + } + menu={ + + + + } + /> + ) +} + +interface ExpandableTimelineItemProps { + chunk: Chunk + onSelect: (chunk: Chunk) => void + selectedChunkId?: string + /** + * Chunks that are squashed together on publish. + * e.g. all the draft mutations are squashed into a single `publish` chunk when the document is published. + */ + squashedChunks: Chunk[] +} + +export function ExpandableTimelineItem(props: ExpandableTimelineItemProps) { + const {selectedChunkId, squashedChunks, chunk, onSelect} = props + const [isExpanded, setIsExpanded] = useState( + // If the selected chunk is a squashed chunk, expand the item + () => !!squashedChunks.find((c) => c.id === selectedChunkId), + ) + const chunkId = chunk.id + const authorUserIds = Array.from(chunk.authors) + + const optionsMenu = useMemo( + () => + squashedChunks.length > 1 ? ( + setIsExpanded(!isExpanded)} + /> + ) : null, + [isExpanded, chunkId, squashedChunks.length], + ) + const collaborators = Array.from( + new Set(squashedChunks?.flatMap((c) => Array.from(c.authors)) || []), + ).filter((id) => !authorUserIds.includes(id)) + + return ( + <> + + {squashedChunks && squashedChunks.length > 1 && ( + + {isExpanded && + squashedChunks.map((squashedChunk) => ( + + + + ))} + + )} + + ) +} diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx index 23348099ef1..85d6bb552b9 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx @@ -8,8 +8,10 @@ import { useTranslation, } from 'sanity' +import {ExpandableTimelineItem} from './expandableTimelineItem' import {ListWrapper, Root, StackWrapper} from './timeline.styled' import {TimelineItem} from './timelineItem' +import {collapseChunksOnPublish, isNonPublishChunk, isPublishChunk} from './utils' interface TimelineProps { chunks: Chunk[] @@ -29,7 +31,7 @@ export const Timeline = ({ chunks, disabledBeforeFirstChunk, hasMoreChunks, - lastChunk, + lastChunk: selectedChunk, onLoadMore, onSelect, firstChunk, @@ -38,37 +40,72 @@ export const Timeline = ({ const [mounted, setMounted] = useState(false) const {t} = useTranslation('studio') + const selectedChunkId = selectedChunk?.id const filteredChunks = useMemo(() => { - return chunks.filter((c) => { - if (disabledBeforeFirstChunk && firstChunk) { - return c.index < firstChunk.index - } - return true - }) + return collapseChunksOnPublish( + chunks.filter((c) => { + if (disabledBeforeFirstChunk && firstChunk) { + return c.index < firstChunk.index + } + return true + }), + ) }, [chunks, disabledBeforeFirstChunk, firstChunk]) + /** + * The index of the selected chunk in the filtered list, or -1 if not found. + * It returns the parent index for publish chunks. + */ const selectedIndex = useMemo( - () => (lastChunk?.id ? filteredChunks.findIndex((c) => c.id === lastChunk.id) : -1), - [lastChunk?.id, filteredChunks], + () => + selectedChunkId + ? filteredChunks.findIndex((chunk) => { + if (isNonPublishChunk(chunk)) { + return chunk.id === selectedChunkId + } + if (isPublishChunk(chunk)) { + const isParentSelected = chunk.id === selectedChunkId + if (isParentSelected) return true + + const isChildrenSelected = chunk.squashedChunks?.find( + (squashed) => squashed.id === selectedChunkId, + ) + return isChildrenSelected + } + return false + }) + : -1, + [selectedChunkId, filteredChunks], ) - const renderItem = useCallback>( + const renderItem = useCallback>( (chunk, {activeIndex}) => { const isFirst = activeIndex === 0 + return ( - + {isPublishChunk(chunk) ? ( + + ) : ( + + )} + {activeIndex === filteredChunks.length - 1 && hasMoreChunks && } ) }, - [filteredChunks, hasMoreChunks, onSelect, selectedIndex], + [filteredChunks.length, hasMoreChunks, onSelect, selectedChunkId], ) useEffect(() => setMounted(true), []) diff --git a/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx b/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx index 79c3652bfa9..46c41a5dc2a 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx @@ -1,11 +1,10 @@ -/* eslint-disable camelcase */ -import {Card, Flex, Menu, Stack, Text} from '@sanity/ui' +import {Card, Flex, Stack, Text} from '@sanity/ui' +// eslint-disable-next-line camelcase import {getTheme_v2, type ThemeColorAvatarColorKey} from '@sanity/ui/theme' import {createElement, type MouseEvent, useCallback, useMemo} from 'react' import { type Chunk, type ChunkType, - ContextMenuButton, type RelativeTimeOptions, useDateTimeFormat, useRelativeTime, @@ -13,8 +12,6 @@ import { } from 'sanity' import {css, styled} from 'styled-components' -import {MenuButton, MenuItem} from '../../../../ui-components' -import {structureLocaleNamespace} from '../../../i18n' import {getTimelineEventIconComponent} from './helpers' import {TIMELINE_ITEM_I18N_KEY_MAPPING} from './timelineI18n' import {UserAvatarStack} from './userAvatarStack' @@ -49,37 +46,14 @@ const TIMELINE_ITEM_EVENT_TONE: Record - } - menu={ - - - - } - /> - ) -} export interface TimelineItemProps { chunk: Chunk isSelected: boolean onSelect: (chunk: Chunk) => void timestamp: string + collaborators?: string[] type: ChunkType - /** - * Chunks that are squashed together on publish. - * e.g. all the draft mutations are squashed into a single `publish` chunk when the document is published. - */ - squashedChunks?: Chunk[] + optionsMenu?: React.ReactNode } const RELATIVE_TIME_OPTIONS: RelativeTimeOptions = { @@ -92,19 +66,14 @@ export function TimelineItem({ isSelected, onSelect, timestamp, + collaborators = [], type, - squashedChunks, + optionsMenu, }: TimelineItemProps) { const {t} = useTranslation('studio') - const iconComponent = getTimelineEventIconComponent(type) const authorUserIds = Array.from(chunk.authors) - // TODO: This will be part of future changes where we will show the history squashed when published - const collaborators = Array.from( - new Set(squashedChunks?.flatMap((c) => Array.from(c.authors)) || []), - ).filter((id) => !authorUserIds.includes(id)) - const isSelectable = type !== 'delete' const dateFormat = useDateTimeFormat({dateStyle: 'medium', timeStyle: 'short'}) const date = new Date(timestamp) @@ -166,7 +135,7 @@ export function TimelineItem({ )} - {squashedChunks && squashedChunks?.length > 1 ? : null} + {optionsMenu} ) } diff --git a/packages/sanity/src/structure/panes/document/timeline/utils.test.ts b/packages/sanity/src/structure/panes/document/timeline/utils.test.ts new file mode 100644 index 00000000000..092d6fc7fe3 --- /dev/null +++ b/packages/sanity/src/structure/panes/document/timeline/utils.test.ts @@ -0,0 +1,230 @@ +import {describe, expect, it} from '@jest/globals' +import {type Chunk} from 'sanity' + +import {collapseChunksOnPublish} from './utils' + +const chunks: Chunk[] = [ + { + index: 6, + id: 'z2633zRhBXUPVFxuhOgS3I', + type: 'publish', + start: 5, + end: 6, + startTimestamp: '2024-09-02T09:28:49.734Z', + endTimestamp: '2024-09-02T09:28:49.734Z', + authors: new Set(['author1']), + draftState: 'missing', + publishedState: 'present', + }, + { + index: 5, + id: '319b9969-9134-43db-912b-cf3c0082c2bc', + type: 'editDraft', + start: 1, + end: 5, + startTimestamp: '2024-09-02T09:28:34.522Z', + endTimestamp: '2024-09-02T09:28:39.049Z', + authors: new Set(['author1']), + draftState: 'present', + publishedState: 'present', + }, + { + index: 4, + id: '0181e905-db87-4a71-9b8d-dc61c3281686', + type: 'editDraft', + start: -1, + end: 1, + startTimestamp: '2024-08-29T12:28:01.286194Z', + endTimestamp: '2024-08-29T12:28:03.508054Z', + authors: new Set(['author2']), + draftState: 'present', + publishedState: 'present', + }, + { + index: 3, + id: 'oizpdYkKQhBxlL6mF9cm6g', + type: 'publish', + start: -2, + end: -1, + startTimestamp: '2024-08-28T07:42:56.954657Z', + endTimestamp: '2024-08-28T07:42:56.954657Z', + authors: new Set(['author3']), + + draftState: 'missing', + publishedState: 'present', + }, + { + index: 2, + id: '058afb19-b9f2-416a-b6a0-e02600f22d5c', + type: 'editDraft', + start: -5, + end: -2, + startTimestamp: '2024-08-21T18:50:46.872241Z', + endTimestamp: '2024-08-21T18:50:50.921116Z', + authors: new Set(['author1']), + draftState: 'present', + publishedState: 'unknown', + }, + { + index: 1, + id: 'a319e276-8fcb-463c-ad88-cc40d9bed20e', + type: 'editDraft', + start: -7, + end: -5, + startTimestamp: '2024-08-21T01:21:44.156523Z', + endTimestamp: '2024-08-21T01:21:45.599240Z', + authors: new Set(['author2']), + draftState: 'present', + publishedState: 'unknown', + }, + { + index: 0, + id: '1dc76dd9-c852-4e5d-b2a1-a4e0ea6bad9c', + type: 'editDraft', + start: -9, + end: -7, + startTimestamp: '2024-08-20T16:15:45.198871Z', + endTimestamp: '2024-08-20T16:15:47.960919Z', + authors: new Set(['author3']), + draftState: 'present', + publishedState: 'unknown', + }, + { + index: -1, + id: '@initial', + type: 'initial', + start: -9, + end: -9, + startTimestamp: '2024-08-20T16:15:45.198871Z', + endTimestamp: '2024-08-20T16:15:45.198871Z', + authors: new Set(['author0']), + draftState: 'present', + publishedState: 'unknown', + }, +] + +describe('Tests collpaseChunksOnPublish', () => { + it('should collapse the editDraft chunks into the single publish chunk', () => { + const collapsedChunks = collapseChunksOnPublish(chunks) + expect(collapsedChunks).toMatchInlineSnapshot(` + Array [ + Object { + "authors": Set { + "author1", + }, + "collapsedChunks": Array [ + Object { + "authors": Set { + "author1", + }, + "draftState": "present", + "end": 5, + "endTimestamp": "2024-09-02T09:28:39.049Z", + "id": "319b9969-9134-43db-912b-cf3c0082c2bc", + "index": 5, + "publishedState": "present", + "start": 1, + "startTimestamp": "2024-09-02T09:28:34.522Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author2", + }, + "draftState": "present", + "end": 1, + "endTimestamp": "2024-08-29T12:28:03.508054Z", + "id": "0181e905-db87-4a71-9b8d-dc61c3281686", + "index": 4, + "publishedState": "present", + "start": -1, + "startTimestamp": "2024-08-29T12:28:01.286194Z", + "type": "editDraft", + }, + ], + "draftState": "missing", + "end": 6, + "endTimestamp": "2024-09-02T09:28:49.734Z", + "id": "z2633zRhBXUPVFxuhOgS3I", + "index": 6, + "publishedState": "present", + "start": 5, + "startTimestamp": "2024-09-02T09:28:49.734Z", + "type": "publish", + }, + Object { + "authors": Set { + "author3", + }, + "collapsedChunks": Array [ + Object { + "authors": Set { + "author1", + }, + "draftState": "present", + "end": -2, + "endTimestamp": "2024-08-21T18:50:50.921116Z", + "id": "058afb19-b9f2-416a-b6a0-e02600f22d5c", + "index": 2, + "publishedState": "unknown", + "start": -5, + "startTimestamp": "2024-08-21T18:50:46.872241Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author2", + }, + "draftState": "present", + "end": -5, + "endTimestamp": "2024-08-21T01:21:45.599240Z", + "id": "a319e276-8fcb-463c-ad88-cc40d9bed20e", + "index": 1, + "publishedState": "unknown", + "start": -7, + "startTimestamp": "2024-08-21T01:21:44.156523Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author3", + }, + "draftState": "present", + "end": -7, + "endTimestamp": "2024-08-20T16:15:47.960919Z", + "id": "1dc76dd9-c852-4e5d-b2a1-a4e0ea6bad9c", + "index": 0, + "publishedState": "unknown", + "start": -9, + "startTimestamp": "2024-08-20T16:15:45.198871Z", + "type": "editDraft", + }, + ], + "draftState": "missing", + "end": -1, + "endTimestamp": "2024-08-28T07:42:56.954657Z", + "id": "oizpdYkKQhBxlL6mF9cm6g", + "index": 3, + "publishedState": "present", + "start": -2, + "startTimestamp": "2024-08-28T07:42:56.954657Z", + "type": "publish", + }, + Object { + "authors": Set { + "author0", + }, + "draftState": "present", + "end": -9, + "endTimestamp": "2024-08-20T16:15:45.198871Z", + "id": "@initial", + "index": -1, + "publishedState": "unknown", + "start": -9, + "startTimestamp": "2024-08-20T16:15:45.198871Z", + "type": "initial", + }, + ] + `) + }) +}) diff --git a/packages/sanity/src/structure/panes/document/timeline/utils.ts b/packages/sanity/src/structure/panes/document/timeline/utils.ts new file mode 100644 index 00000000000..0f0af513a8a --- /dev/null +++ b/packages/sanity/src/structure/panes/document/timeline/utils.ts @@ -0,0 +1,44 @@ +import {type Chunk, type ChunkType} from 'sanity' + +type NonPublishChunk = Omit & { + type: Exclude +} +export type PublishChunk = Omit & { + type: 'publish' + squashedChunks: Chunk[] +} +export const isNonPublishChunk = (chunk: Chunk): chunk is NonPublishChunk => + chunk.type !== 'publish' + +export const isPublishChunk = (chunk: Chunk): chunk is PublishChunk => chunk.type === 'publish' + +export type ChunksWithCollapsedDrafts = NonPublishChunk | PublishChunk +/** + * Takes an array of chunks and collapses all the changes to the drafts (edits) into the published chunk + */ +export function collapseChunksOnPublish(chunks: Chunk[]): ChunksWithCollapsedDrafts[] { + const result: ChunksWithCollapsedDrafts[] = [] + + for (const chunk of chunks) { + if (chunk.type === 'publish') { + result.push({ + ...chunk, + type: 'publish', + squashedChunks: [], // Initialize the squashedChunks array + }) + continue + } + if (chunk.type === 'editDraft') { + const lastChunk = result[result.length - 1] + if (lastChunk?.type === 'publish') { + lastChunk.squashedChunks.push(chunk) + continue + } + } + if (isNonPublishChunk(chunk)) { + result.push(chunk) + } + } + + return result +} From 85f0782ff2ef1f3110728a9eb5b8b9533d343bdc Mon Sep 17 00:00:00 2001 From: pedrobonamin Date: Mon, 2 Sep 2024 18:56:30 +0200 Subject: [PATCH 2/4] feat(structure): refactor expandable item, all items need to be rendered in the same virtual list --- .../inspectors/changes/HistorySelector.tsx | 1 - .../__workshop__/TimelineItemStory.tsx | 202 ++++++++++++------ .../timeline/expandableTimelineItem.tsx | 133 ------------ .../timeline/expandableTimelineItemMenu.tsx | 50 +++++ .../panes/document/timeline/timeline.tsx | 120 ++++++----- .../panes/document/timeline/timelineItem.tsx | 15 +- .../panes/document/timeline/timelineMenu.tsx | 53 +++-- .../panes/document/timeline/utils.test.ts | 167 ++++++++------- .../panes/document/timeline/utils.ts | 59 ++++- 9 files changed, 439 insertions(+), 361 deletions(-) delete mode 100644 packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx create mode 100644 packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx diff --git a/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx b/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx index 62984f06054..6689070e671 100644 --- a/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx +++ b/packages/sanity/src/structure/panes/document/inspectors/changes/HistorySelector.tsx @@ -74,7 +74,6 @@ export function HistorySelector({showList}: {showList: boolean}) { showList ? ( (() => new Date()) const [selected, setSelected] = useState(null) const dateFormatter = useDateTimeFormat({dateStyle: 'medium', timeStyle: 'short'}) const calendarLabels = useMemo(() => getCalendarLabels(coreT), [coreT]) - const inputValue = date ? dateFormatter.format(new Date(date)) : '' const handleDatechange = (newDate: Date | null) => { if (newDate) { @@ -76,43 +186,13 @@ export default function TimelineItemStory() { - - {CHUNK_TYPES.map((type, index) => { - if (type === 'publish') { - return ( - - ) - } - return ( - - ) - })} - + ({...chunk, endTimestamp: date.toString()}))} + hasMoreChunks={false} + lastChunk={selected ? CHUNKS.find((chunk) => chunk.id === selected) : undefined} + onSelect={handleSelect} + onLoadMore={() => {}} + /> diff --git a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx deleted file mode 100644 index 35763cccf20..00000000000 --- a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItem.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import {Box, Menu} from '@sanity/ui' -import {type MouseEvent, useCallback, useMemo, useState} from 'react' -import {type Chunk, ContextMenuButton, useTranslation} from 'sanity' -import {styled} from 'styled-components' - -import {MenuButton, MenuItem} from '../../../../ui-components' -import {structureLocaleNamespace} from '../../../i18n' -import {TimelineItem} from './timelineItem' - -const ExpandedWrapper = styled.div` - display: grid; - grid-template-columns: 1fr; - grid-auto-rows: 0fr; - overflow: hidden; - transition: all 200ms ease; - padding-left: 16px; - &[data-expanded] { - grid-auto-rows: 1fr; - padding-top: 4px; - padding-bottom: 4px; - gap: 4px; - } -` - -function TimelineItemMenu({ - chunkId, - isExpanded, - onExpandClick, -}: { - chunkId: string - isExpanded: boolean - onExpandClick: () => void -}) { - const {t} = useTranslation(structureLocaleNamespace) - const handleExpandClick = useCallback( - (e: MouseEvent) => { - // Avoid the click event to propagate to the parent button and closing the popover - e.stopPropagation() - onExpandClick() - }, - [onExpandClick], - ) - return ( - - } - menu={ - - - - } - /> - ) -} - -interface ExpandableTimelineItemProps { - chunk: Chunk - onSelect: (chunk: Chunk) => void - selectedChunkId?: string - /** - * Chunks that are squashed together on publish. - * e.g. all the draft mutations are squashed into a single `publish` chunk when the document is published. - */ - squashedChunks: Chunk[] -} - -export function ExpandableTimelineItem(props: ExpandableTimelineItemProps) { - const {selectedChunkId, squashedChunks, chunk, onSelect} = props - const [isExpanded, setIsExpanded] = useState( - // If the selected chunk is a squashed chunk, expand the item - () => !!squashedChunks.find((c) => c.id === selectedChunkId), - ) - const chunkId = chunk.id - const authorUserIds = Array.from(chunk.authors) - - const optionsMenu = useMemo( - () => - squashedChunks.length > 1 ? ( - setIsExpanded(!isExpanded)} - /> - ) : null, - [isExpanded, chunkId, squashedChunks.length], - ) - const collaborators = Array.from( - new Set(squashedChunks?.flatMap((c) => Array.from(c.authors)) || []), - ).filter((id) => !authorUserIds.includes(id)) - - return ( - <> - - {squashedChunks && squashedChunks.length > 1 && ( - - {isExpanded && - squashedChunks.map((squashedChunk) => ( - - - - ))} - - )} - - ) -} diff --git a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx new file mode 100644 index 00000000000..7cc5a4723cf --- /dev/null +++ b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx @@ -0,0 +1,50 @@ +import {Menu} from '@sanity/ui' +import {type MouseEvent, useCallback} from 'react' +import {ContextMenuButton, useTranslation} from 'sanity' + +import {MenuButton, MenuItem} from '../../../../ui-components' +import {structureLocaleNamespace} from '../../../i18n' + +export function ExpandableTimelineItemMenu({ + chunkId, + isExpanded, + onExpand, +}: { + chunkId: string + isExpanded: boolean + onExpand: () => void +}) { + const {t} = useTranslation(structureLocaleNamespace) + const handleExpandClick = useCallback( + (e: MouseEvent) => { + // TODO: Avoid the click event to propagate to the parent button and closing the popover + e.stopPropagation() + onExpand() + }, + [onExpand], + ) + return ( + + } + menu={ + + + + } + /> + ) +} diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx index 85d6bb552b9..edbc9d0b4a4 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx @@ -8,15 +8,14 @@ import { useTranslation, } from 'sanity' -import {ExpandableTimelineItem} from './expandableTimelineItem' +import {ExpandableTimelineItemMenu} from './expandableTimelineItemMenu' import {ListWrapper, Root, StackWrapper} from './timeline.styled' import {TimelineItem} from './timelineItem' -import {collapseChunksOnPublish, isNonPublishChunk, isPublishChunk} from './utils' +import {addChunksMetadata, isNonPublishChunk, isPublishChunk} from './utils' interface TimelineProps { chunks: Chunk[] - disabledBeforeFirstChunk?: boolean - firstChunk?: Chunk | null + hasMoreChunks: boolean | null lastChunk?: Chunk | null onLoadMore: () => void @@ -29,52 +28,59 @@ interface TimelineProps { export const Timeline = ({ chunks, - disabledBeforeFirstChunk, hasMoreChunks, lastChunk: selectedChunk, onLoadMore, onSelect, - firstChunk, listMaxHeight = 'calc(100vh - 198px)', }: TimelineProps) => { const [mounted, setMounted] = useState(false) const {t} = useTranslation('studio') - const selectedChunkId = selectedChunk?.id + const chunksWithMetadata = useMemo(() => addChunksMetadata(chunks), [chunks]) + + const [expandedParents, setExpandedParents] = useState>(() => { + if (selectedChunkId) { + const selectedChunkWithParentData = chunksWithMetadata.find( + (chunk) => chunk.id === selectedChunkId, + ) + if ( + selectedChunkWithParentData && + isNonPublishChunk(selectedChunkWithParentData) && + selectedChunkWithParentData.parentId + ) { + return new Set([selectedChunkWithParentData.parentId]) + } + } + return new Set() + }) + const filteredChunks = useMemo(() => { - return collapseChunksOnPublish( - chunks.filter((c) => { - if (disabledBeforeFirstChunk && firstChunk) { - return c.index < firstChunk.index + return chunksWithMetadata.filter((chunk) => { + if (isPublishChunk(chunk) || !chunk.parentId) return true + return expandedParents.has(chunk.parentId) + }) + }, [chunksWithMetadata, expandedParents]) + + const handleExpandParent = useCallback( + (parentId: string) => () => + setExpandedParents((prev) => { + if (prev.has(parentId)) { + const next = new Set(prev) + next.delete(parentId) + return next } - return true + + const next = new Set(prev) + next.add(parentId) + return next }), - ) - }, [chunks, disabledBeforeFirstChunk, firstChunk]) + [], + ) - /** - * The index of the selected chunk in the filtered list, or -1 if not found. - * It returns the parent index for publish chunks. - */ const selectedIndex = useMemo( () => - selectedChunkId - ? filteredChunks.findIndex((chunk) => { - if (isNonPublishChunk(chunk)) { - return chunk.id === selectedChunkId - } - if (isPublishChunk(chunk)) { - const isParentSelected = chunk.id === selectedChunkId - if (isParentSelected) return true - - const isChildrenSelected = chunk.squashedChunks?.find( - (squashed) => squashed.id === selectedChunkId, - ) - return isChildrenSelected - } - return false - }) - : -1, + selectedChunkId ? filteredChunks.findIndex((chunk) => chunk.id === selectedChunkId) : -1, [selectedChunkId, filteredChunks], ) @@ -83,29 +89,41 @@ export const Timeline = ({ const isFirst = activeIndex === 0 return ( - - {isPublishChunk(chunk) ? ( - - ) : ( + <> + 0 ? ( + + ) : null + } /> - )} - + {activeIndex === filteredChunks.length - 1 && hasMoreChunks && } - + ) }, - [filteredChunks.length, hasMoreChunks, onSelect, selectedChunkId], + [ + expandedParents, + filteredChunks.length, + handleExpandParent, + hasMoreChunks, + onSelect, + selectedChunkId, + ], ) useEffect(() => setMounted(true), []) @@ -142,7 +160,7 @@ export const Timeline = ({ autoFocus="list" initialIndex={selectedIndex} initialScrollAlign="center" - itemHeight={40} + itemHeight={57} items={filteredChunks} onEndReached={onLoadMore} onEndReachedIndexOffset={20} diff --git a/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx b/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx index 46c41a5dc2a..67042e285f9 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timelineItem.tsx @@ -50,9 +50,7 @@ export interface TimelineItemProps { chunk: Chunk isSelected: boolean onSelect: (chunk: Chunk) => void - timestamp: string - collaborators?: string[] - type: ChunkType + collaborators?: Set optionsMenu?: React.ReactNode } @@ -65,15 +63,14 @@ export function TimelineItem({ chunk, isSelected, onSelect, - timestamp, - collaborators = [], - type, + collaborators, optionsMenu, }: TimelineItemProps) { const {t} = useTranslation('studio') + const {type, endTimestamp: timestamp} = chunk const iconComponent = getTimelineEventIconComponent(type) const authorUserIds = Array.from(chunk.authors) - + const collaboratorsUsersIds = collaborators ? Array.from(collaborators) : [] const isSelectable = type !== 'delete' const dateFormat = useDateTimeFormat({dateStyle: 'medium', timeStyle: 'short'}) const date = new Date(timestamp) @@ -128,9 +125,9 @@ export function TimelineItem({ - {collaborators.length > 0 && ( + {collaboratorsUsersIds.length > 0 && ( - + )} diff --git a/packages/sanity/src/structure/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/structure/panes/document/timeline/timelineMenu.tsx index b3547f898fd..8ea481685e5 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timelineMenu.tsx @@ -7,7 +7,7 @@ import { useGlobalKeyDown, useToast, } from '@sanity/ui' -import {useCallback, useRef, useState} from 'react' +import {useCallback, useMemo, useRef, useState} from 'react' import {type Chunk, useTimelineSelector, useTranslation} from 'sanity' import {styled} from 'styled-components' @@ -108,33 +108,44 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { } }, [loading, timelineStore]) - const content = timelineError ? ( - - ) : ( - <> - {mode === 'rev' && ( + const content = useMemo(() => { + if (timelineError) return + + if (mode === 'rev') { + return ( - )} - {mode === 'since' && ( - - )} - - ) + ) + } + + const filteredChunks = realRevChunk + ? chunks.filter((c) => c.index < realRevChunk.index) + : chunks + return ( + + ) + }, [ + chunks, + handleLoadMore, + hasMoreChunks, + mode, + realRevChunk, + selectRev, + selectSince, + sinceTime, + timelineError, + ]) const formatParams = { timestamp: {dateStyle: 'medium', timeStyle: 'short'}, diff --git a/packages/sanity/src/structure/panes/document/timeline/utils.test.ts b/packages/sanity/src/structure/panes/document/timeline/utils.test.ts index 092d6fc7fe3..6c2c99a8b37 100644 --- a/packages/sanity/src/structure/panes/document/timeline/utils.test.ts +++ b/packages/sanity/src/structure/panes/document/timeline/utils.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from '@jest/globals' import {type Chunk} from 'sanity' -import {collapseChunksOnPublish} from './utils' +import {addChunksMetadata} from './utils' const chunks: Chunk[] = [ { @@ -103,45 +103,22 @@ const chunks: Chunk[] = [ }, ] -describe('Tests collpaseChunksOnPublish', () => { +describe('Tests addChunksMetadata', () => { it('should collapse the editDraft chunks into the single publish chunk', () => { - const collapsedChunks = collapseChunksOnPublish(chunks) + const collapsedChunks = addChunksMetadata(chunks) expect(collapsedChunks).toMatchInlineSnapshot(` Array [ Object { "authors": Set { "author1", }, - "collapsedChunks": Array [ - Object { - "authors": Set { - "author1", - }, - "draftState": "present", - "end": 5, - "endTimestamp": "2024-09-02T09:28:39.049Z", - "id": "319b9969-9134-43db-912b-cf3c0082c2bc", - "index": 5, - "publishedState": "present", - "start": 1, - "startTimestamp": "2024-09-02T09:28:34.522Z", - "type": "editDraft", - }, - Object { - "authors": Set { - "author2", - }, - "draftState": "present", - "end": 1, - "endTimestamp": "2024-08-29T12:28:03.508054Z", - "id": "0181e905-db87-4a71-9b8d-dc61c3281686", - "index": 4, - "publishedState": "present", - "start": -1, - "startTimestamp": "2024-08-29T12:28:01.286194Z", - "type": "editDraft", - }, + "children": Array [ + "319b9969-9134-43db-912b-cf3c0082c2bc", + "0181e905-db87-4a71-9b8d-dc61c3281686", ], + "collaborators": Set { + "author2", + }, "draftState": "missing", "end": 6, "endTimestamp": "2024-09-02T09:28:49.734Z", @@ -152,54 +129,49 @@ describe('Tests collpaseChunksOnPublish', () => { "startTimestamp": "2024-09-02T09:28:49.734Z", "type": "publish", }, + Object { + "authors": Set { + "author1", + }, + "draftState": "present", + "end": 5, + "endTimestamp": "2024-09-02T09:28:39.049Z", + "id": "319b9969-9134-43db-912b-cf3c0082c2bc", + "index": 5, + "parentId": "z2633zRhBXUPVFxuhOgS3I", + "publishedState": "present", + "start": 1, + "startTimestamp": "2024-09-02T09:28:34.522Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author2", + }, + "draftState": "present", + "end": 1, + "endTimestamp": "2024-08-29T12:28:03.508054Z", + "id": "0181e905-db87-4a71-9b8d-dc61c3281686", + "index": 4, + "parentId": "z2633zRhBXUPVFxuhOgS3I", + "publishedState": "present", + "start": -1, + "startTimestamp": "2024-08-29T12:28:01.286194Z", + "type": "editDraft", + }, Object { "authors": Set { "author3", }, - "collapsedChunks": Array [ - Object { - "authors": Set { - "author1", - }, - "draftState": "present", - "end": -2, - "endTimestamp": "2024-08-21T18:50:50.921116Z", - "id": "058afb19-b9f2-416a-b6a0-e02600f22d5c", - "index": 2, - "publishedState": "unknown", - "start": -5, - "startTimestamp": "2024-08-21T18:50:46.872241Z", - "type": "editDraft", - }, - Object { - "authors": Set { - "author2", - }, - "draftState": "present", - "end": -5, - "endTimestamp": "2024-08-21T01:21:45.599240Z", - "id": "a319e276-8fcb-463c-ad88-cc40d9bed20e", - "index": 1, - "publishedState": "unknown", - "start": -7, - "startTimestamp": "2024-08-21T01:21:44.156523Z", - "type": "editDraft", - }, - Object { - "authors": Set { - "author3", - }, - "draftState": "present", - "end": -7, - "endTimestamp": "2024-08-20T16:15:47.960919Z", - "id": "1dc76dd9-c852-4e5d-b2a1-a4e0ea6bad9c", - "index": 0, - "publishedState": "unknown", - "start": -9, - "startTimestamp": "2024-08-20T16:15:45.198871Z", - "type": "editDraft", - }, + "children": Array [ + "058afb19-b9f2-416a-b6a0-e02600f22d5c", + "a319e276-8fcb-463c-ad88-cc40d9bed20e", + "1dc76dd9-c852-4e5d-b2a1-a4e0ea6bad9c", ], + "collaborators": Set { + "author1", + "author2", + }, "draftState": "missing", "end": -1, "endTimestamp": "2024-08-28T07:42:56.954657Z", @@ -210,6 +182,51 @@ describe('Tests collpaseChunksOnPublish', () => { "startTimestamp": "2024-08-28T07:42:56.954657Z", "type": "publish", }, + Object { + "authors": Set { + "author1", + }, + "draftState": "present", + "end": -2, + "endTimestamp": "2024-08-21T18:50:50.921116Z", + "id": "058afb19-b9f2-416a-b6a0-e02600f22d5c", + "index": 2, + "parentId": "oizpdYkKQhBxlL6mF9cm6g", + "publishedState": "unknown", + "start": -5, + "startTimestamp": "2024-08-21T18:50:46.872241Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author2", + }, + "draftState": "present", + "end": -5, + "endTimestamp": "2024-08-21T01:21:45.599240Z", + "id": "a319e276-8fcb-463c-ad88-cc40d9bed20e", + "index": 1, + "parentId": "oizpdYkKQhBxlL6mF9cm6g", + "publishedState": "unknown", + "start": -7, + "startTimestamp": "2024-08-21T01:21:44.156523Z", + "type": "editDraft", + }, + Object { + "authors": Set { + "author3", + }, + "draftState": "present", + "end": -7, + "endTimestamp": "2024-08-20T16:15:47.960919Z", + "id": "1dc76dd9-c852-4e5d-b2a1-a4e0ea6bad9c", + "index": 0, + "parentId": "oizpdYkKQhBxlL6mF9cm6g", + "publishedState": "unknown", + "start": -9, + "startTimestamp": "2024-08-20T16:15:45.198871Z", + "type": "editDraft", + }, Object { "authors": Set { "author0", diff --git a/packages/sanity/src/structure/panes/document/timeline/utils.ts b/packages/sanity/src/structure/panes/document/timeline/utils.ts index 0f0af513a8a..12d7377a2c2 100644 --- a/packages/sanity/src/structure/panes/document/timeline/utils.ts +++ b/packages/sanity/src/structure/panes/document/timeline/utils.ts @@ -1,37 +1,76 @@ import {type Chunk, type ChunkType} from 'sanity' -type NonPublishChunk = Omit & { +export type NonPublishChunk = Omit & { type: Exclude + parentId?: string } + export type PublishChunk = Omit & { type: 'publish' - squashedChunks: Chunk[] + children: string[] + collaborators: Set } + export const isNonPublishChunk = (chunk: Chunk): chunk is NonPublishChunk => chunk.type !== 'publish' export const isPublishChunk = (chunk: Chunk): chunk is PublishChunk => chunk.type === 'publish' +/** + * searches for the previous publish action in the list of chunks + * e.g. chunks = [publish, edit, publish, edit, edit] it needs to return the second publish action + * e.g. chunks = [publish, edit, delete, edit, edit] it returns undefined + */ + +function getPreviousPublishAction(chunks: Chunk[]) { + let previousPublish: PublishChunk | null = null + // We need to iterate from the end to the start of the list + for (let index = chunks.length - 1; index >= 0; index--) { + const chunk = chunks[index] + if (isPublishChunk(chunk)) { + previousPublish = chunk + break + } + if (chunk.type === 'editDraft') { + continue + } else break + } + + return previousPublish +} export type ChunksWithCollapsedDrafts = NonPublishChunk | PublishChunk + /** - * Takes an array of chunks and collapses all the changes to the drafts (edits) into the published chunk + * Takes an array of chunks and adds them metadata necessary for the timeline view. + * for draft chunks, it will add the parentId of the published chunk if this draft action is now published + * for published, it will add the children array and the collaborators array */ -export function collapseChunksOnPublish(chunks: Chunk[]): ChunksWithCollapsedDrafts[] { +export function addChunksMetadata(chunks: Chunk[]): ChunksWithCollapsedDrafts[] { const result: ChunksWithCollapsedDrafts[] = [] for (const chunk of chunks) { - if (chunk.type === 'publish') { + if (isPublishChunk(chunk)) { result.push({ ...chunk, type: 'publish', - squashedChunks: [], // Initialize the squashedChunks array + children: [], + collaborators: new Set(), // Initialize the collaborators array }) continue } - if (chunk.type === 'editDraft') { - const lastChunk = result[result.length - 1] - if (lastChunk?.type === 'publish') { - lastChunk.squashedChunks.push(chunk) + if (isNonPublishChunk(chunk)) { + const previousPublish = getPreviousPublishAction(result) + if (chunk.type === 'editDraft' && previousPublish?.type === 'publish') { + Array.from(chunk.authors).forEach((id) => { + if (!previousPublish.authors.has(id)) { + previousPublish.collaborators.add(id) + } + }) + previousPublish.children.push(chunk.id) + result.push({ + ...chunk, + parentId: previousPublish.id, + }) continue } } From bb4cbfdc6280d5c9a551bdd856e147c1ea41ca91 Mon Sep 17 00:00:00 2001 From: pedrobonamin Date: Tue, 3 Sep 2024 10:46:43 +0200 Subject: [PATCH 3/4] feat(structure): expand elements on menu click --- .../timeline/expandableTimelineItemMenu.tsx | 44 +++++++++++- .../panes/document/timeline/timeline.tsx | 6 +- .../panes/document/timeline/timelineMenu.tsx | 71 ++++++++++--------- 3 files changed, 84 insertions(+), 37 deletions(-) diff --git a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx index 7cc5a4723cf..45139114fde 100644 --- a/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/expandableTimelineItemMenu.tsx @@ -1,9 +1,38 @@ -import {Menu} from '@sanity/ui' +import {Menu, usePortal} from '@sanity/ui' import {type MouseEvent, useCallback} from 'react' import {ContextMenuButton, useTranslation} from 'sanity' import {MenuButton, MenuItem} from '../../../../ui-components' import {structureLocaleNamespace} from '../../../i18n' +import {TIMELINE_LIST_WRAPPER_ID} from './timeline' +import {TIMELINE_MENU_PORTAL} from './timelineMenu' + +/** + * This is a hack to force the scrollbar to not appear when the list is expanding, + * if we don't do this the scrollbar will appear for a brief moment when the list is expanding and then disappear + * when the list is fully expanded. + */ +function hideScrollbarOnExpand(isExpanded: boolean) { + // Do nothing if the list is already expanded + if (isExpanded) return + + const listWrapper = document.getElementById(TIMELINE_LIST_WRAPPER_ID) + + if (listWrapper) { + const firstChildren = listWrapper.children[0] as HTMLElement + const hasScrollbar = firstChildren.scrollHeight > firstChildren.clientHeight + if (!hasScrollbar) { + // + const currentStyle = getComputedStyle(firstChildren).overflowY + // Add overflow hidden to the listWrapper to avoid the scrollbar to appear when expanding + firstChildren.style.overflowY = 'hidden' + setTimeout(() => { + // Reset the overflow style after the list is expanded + firstChildren.style.overflowY = currentStyle + }, 0) + } + } +} export function ExpandableTimelineItemMenu({ chunkId, @@ -15,14 +44,17 @@ export function ExpandableTimelineItemMenu({ onExpand: () => void }) { const {t} = useTranslation(structureLocaleNamespace) + const portalContext = usePortal() + const handleExpandClick = useCallback( (e: MouseEvent) => { - // TODO: Avoid the click event to propagate to the parent button and closing the popover e.stopPropagation() + hideScrollbarOnExpand(isExpanded) onExpand() }, - [onExpand], + [onExpand, isExpanded], ) + return ( } + popover={{ + // when used inside the timeline menu we want to keep the element inside the popover, to avoid closing the popover when clicking expand. + portal: portalContext.elements?.[TIMELINE_MENU_PORTAL] ? TIMELINE_MENU_PORTAL : true, + placement: 'bottom-end', + fallbackPlacements: ['left', 'left-end', 'left-start'], + }} /> ) } diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx index edbc9d0b4a4..b7dc3e8be36 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx @@ -26,13 +26,15 @@ interface TimelineProps { listMaxHeight?: string } +export const TIMELINE_LIST_WRAPPER_ID = 'timeline-list-wrapper' + export const Timeline = ({ chunks, hasMoreChunks, lastChunk: selectedChunk, onLoadMore, onSelect, - listMaxHeight = 'calc(100vh - 198px)', + listMaxHeight = 'calc(100vh - 280px)', }: TimelineProps) => { const [mounted, setMounted] = useState(false) const {t} = useTranslation('studio') @@ -153,7 +155,7 @@ export const Timeline = ({ )} {filteredChunks.length > 0 && ( - + (null) - const popoverRef = useRef(null) + const [popoverRef, setPopoverRef] = useState(null) + const toast = useToast() const chunks = useTimelineSelector(timelineStore, (state) => state.chunks) @@ -64,7 +68,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ) useGlobalKeyDown(handleGlobalKeyDown) - useClickOutsideEvent(open && handleClose, () => [button, popoverRef.current]) + useClickOutsideEvent(open && handleClose, () => [button, popoverRef]) const selectRev = useCallback( (revChunk: Chunk) => { @@ -169,35 +173,38 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { const buttonLabel = mode === 'rev' ? revLabel : sinceLabel return ( - - - + + + ) } From 8343d67918d06f89f2474d267322037b8e0bd168 Mon Sep 17 00:00:00 2001 From: pedrobonamin Date: Tue, 3 Sep 2024 11:23:51 +0200 Subject: [PATCH 4/4] chore(structure): clean timeline item --- .../panes/document/timeline/timeline.tsx | 72 ++++++++----------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx index b7dc3e8be36..f2479cf4183 100644 --- a/packages/sanity/src/structure/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/structure/panes/document/timeline/timeline.tsx @@ -15,7 +15,6 @@ import {addChunksMetadata, isNonPublishChunk, isPublishChunk} from './utils' interface TimelineProps { chunks: Chunk[] - hasMoreChunks: boolean | null lastChunk?: Chunk | null onLoadMore: () => void @@ -43,15 +42,10 @@ export const Timeline = ({ const [expandedParents, setExpandedParents] = useState>(() => { if (selectedChunkId) { - const selectedChunkWithParentData = chunksWithMetadata.find( - (chunk) => chunk.id === selectedChunkId, - ) - if ( - selectedChunkWithParentData && - isNonPublishChunk(selectedChunkWithParentData) && - selectedChunkWithParentData.parentId - ) { - return new Set([selectedChunkWithParentData.parentId]) + // If the selected chunk is a draft, we need to expand its parent + const selected = chunksWithMetadata.find((chunk) => chunk.id === selectedChunkId) + if (selected && isNonPublishChunk(selected) && selected.parentId) { + return new Set([selected.parentId]) } } return new Set() @@ -60,6 +54,7 @@ export const Timeline = ({ const filteredChunks = useMemo(() => { return chunksWithMetadata.filter((chunk) => { if (isPublishChunk(chunk) || !chunk.parentId) return true + // If the chunk has a parent id keep it hidden until the parent is expanded. return expandedParents.has(chunk.parentId) }) }, [chunksWithMetadata, expandedParents]) @@ -67,14 +62,11 @@ export const Timeline = ({ const handleExpandParent = useCallback( (parentId: string) => () => setExpandedParents((prev) => { - if (prev.has(parentId)) { - const next = new Set(prev) - next.delete(parentId) - return next - } - const next = new Set(prev) - next.add(parentId) + + if (prev.has(parentId)) next.delete(parentId) + else next.add(parentId) + return next }), [], @@ -91,31 +83,29 @@ export const Timeline = ({ const isFirst = activeIndex === 0 return ( - <> - - 0 ? ( - - ) : null - } - /> - + + 0 ? ( + + ) : null + } + /> {activeIndex === filteredChunks.length - 1 && hasMoreChunks && } - + ) }, [