From 991c15b815d5654982cd67b1a1733f429b060c32 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Sun, 27 Oct 2024 20:08:32 +0000 Subject: [PATCH 1/7] feat(releases): new global perspective menu item component to support release types --- .../releases/navbar/GlobalPerspectiveMenu.tsx | 86 +++++++-- .../navbar/GlobalPerspectiveMenuItem.tsx | 163 ++++++++++++++++++ .../overview/ReleasesOverviewColumnDefs.tsx | 11 +- 3 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index 8900e21a557..0b43852018e 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -9,11 +9,12 @@ import {useTranslation} from '../../i18n' import {useReleases} from '../../store/release/useReleases' import {ReleaseDetailsDialog} from '../components/dialog/ReleaseDetailsDialog' import {usePerspective} from '../hooks' -import {LATEST} from '../util/const' import {isDraftOrPublished} from '../util/util' +import {GlobalPerspectiveMenuItem} from './GlobalPerspectiveMenuItem' const StyledMenu = styled(Menu)` min-width: 200px; + max-width: 320px; ` const StyledBox = styled(Box)` @@ -21,6 +22,14 @@ const StyledBox = styled(Box)` max-height: 200px; ` +interface LayerRange { + firstIndex: number + lastIndex: number + immediatelyOffset: number + futureOffset: number + neverOffset: number +} + export function GlobalPerspectiveMenu(): JSX.Element { const {deletedReleases, loading, data: releases} = useReleases() const {currentGlobalBundle, setPerspectiveFromRelease, setPerspective} = usePerspective() @@ -59,6 +68,67 @@ export function GlobalPerspectiveMenu(): JSX.Element { setCreateBundleDialogOpen(false) }, []) + const range: LayerRange = useMemo(() => { + let firstIndex = -1 + let lastIndex = 0 + + if (!release.published.hidden) { + firstIndex = 0 + } + + if (current.id === 'published') { + lastIndex = 0 + } + + const immediatelyOffset = 2 + const futureOffset = immediatelyOffset + items.immediately.length + const neverOffset = futureOffset + items.future.length + + for (const item of items.immediately) { + const index = immediatelyOffset + items.immediately.indexOf(item) + + if (firstIndex === -1) { + if (!item.hidden) { + firstIndex = index + } + } + + if (item.name === current.id) { + lastIndex = index + } + } + + for (const item of items.future) { + const index = futureOffset + items.future.indexOf(item) + + if (firstIndex === -1) { + if (!item.hidden) { + firstIndex = index + } + } + + if (item.name === current.id) { + lastIndex = index + } + } + + for (const item of items.never) { + const index = neverOffset + items.never.indexOf(item) + + if (firstIndex === -1) { + if (!item.hidden) { + firstIndex = index + } + } + + if (item.name === current.id) { + lastIndex = index + } + } + + return {firstIndex, lastIndex, immediatelyOffset, futureOffset, neverOffset} + }, [current, items]) + const releasesList = useMemo(() => { if (loading) { return ( @@ -70,16 +140,10 @@ export function GlobalPerspectiveMenu(): JSX.Element { return ( <> - - ) : undefined - } - onClick={() => setPerspective(LATEST._id)} - pressed={false} - text={LATEST.metadata.title} - data-testid="latest-menu-item" + {hasBundles && ( <> diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx new file mode 100644 index 00000000000..283115541d9 --- /dev/null +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx @@ -0,0 +1,163 @@ +import {EyeOpenIcon} from '@sanity/icons' +// eslint-disable-next-line no-restricted-imports +import {Box, Flex, MenuItem, Stack, Text} from '@sanity/ui' +import {type MouseEvent, useCallback} from 'react' +import {getReleaseTone, RelativeTime, ReleaseAvatar, type ReleaseDocument} from 'sanity' +import {styled} from 'styled-components' + +import {usePerspective} from '../hooks/usePerspective' + +const Root = styled.div` + position: relative; + + &[data-in-range]:not([data-last]):after { + content: ''; + display: block; + position: absolute; + left: 14px; + bottom: -4px; + width: 5px; + height: 4px; + background-color: var(--card-border-color); + } + + &[data-in-range] > [data-ui='MenuItem'] { + position: relative; + + &:before { + content: ''; + display: block; + position: absolute; + left: 14px; + top: 0; + width: 5px; + height: 16.5px; + background-color: var(--card-border-color); + } + + &:after { + content: ''; + display: block; + position: absolute; + left: 14px; + top: 16.5px; + bottom: 0; + width: 5px; + background-color: var(--card-border-color); + } + } + + &[data-first] > [data-ui='MenuItem']:before { + display: none; + } + + &[data-last] > [data-ui='MenuItem']:after { + display: none; + } +` + +export function GlobalPerspectiveMenuItem(props: { + release: ReleaseDocument + rangePosition: 'first' | 'within' | 'last' | undefined + toggleable: boolean +}) { + const {release, rangePosition, toggleable} = props + // const {current, replace: replaceVersion, replaceToggle} = usePerspective() + const {currentGlobalBundle, setPerspectiveFromRelease} = usePerspective() + const active = release.name === currentGlobalBundle._id + const first = rangePosition === 'first' + const within = rangePosition === 'within' + const last = rangePosition === 'last' + const inRange = first || within || last + + const handleToggleReleaseVisibility = useCallback((event: MouseEvent) => { + event.stopPropagation() + }, []) + + return ( + + setPerspectiveFromRelease(release._id)} + padding={1} + pressed={active} + > + {/*
*/} + + + + {/* {release.hidden ? ( + + ) : ( */} + + {/* )} */} + + + + + {release.metadata.title} + + {release.metadata.releaseType !== 'undecided' && + (release.publishAt || release.metadata.intendedPublishAt) && ( + + + + )} + + + {!toggleable && ( + + + + + + )} + {/* {toggleable && ( + + )} */} + + + + + ) +} diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx index 4edbb5130ec..0a573ca3035 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx @@ -73,7 +73,7 @@ const ReleaseNameCell: Column['cell'] = ({cellProps, datum: releas const pinButtonIcon = isReleasePinned ? PinFilledIcon : PinIcon return ( - + ( - + ), From c3c580fa153eb22ee730be62229e6b56cf725be2 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 28 Oct 2024 00:20:00 +0000 Subject: [PATCH 2/7] feat(releases): supporting styling for left-track indicator on selected perspective layers --- .../releases/navbar/GlobalPerspectiveMenu.tsx | 148 +++++++++--------- .../navbar/GlobalPerspectiveMenuItem.tsx | 29 ++-- 2 files changed, 87 insertions(+), 90 deletions(-) diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index f85402f54b5..2d49e39dbae 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -39,9 +39,11 @@ const LabelBox = styled(Box)` interface LayerRange { firstIndex: number lastIndex: number - immediatelyOffset: number - futureOffset: number - neverOffset: number + offsets: { + asap: number + scheduled: number + undecided: number + } } function getRangePosition( @@ -69,7 +71,8 @@ function getRangePosition( export function GlobalPerspectiveMenu(): JSX.Element { const {loading, data: releases} = useReleases() - const {currentGlobalBundle, setPerspectiveFromRelease, setPerspective} = usePerspective() + const {currentGlobalBundle} = usePerspective() + const currentGlobalBundleId = currentGlobalBundle._id const [createBundleDialogOpen, setCreateBundleDialogOpen] = useState(false) const styledMenuRef = useRef(null) @@ -84,13 +87,20 @@ export function GlobalPerspectiveMenu(): JSX.Element { setCreateBundleDialogOpen(false) }, []) - const unarchivedReleases = releases.filter((release) => release.state !== 'archived') + const unarchivedReleases = useMemo( + () => releases.filter((release) => release.state !== 'archived'), + [releases], + ) - const releaseTypeReleases = { - asap: unarchivedReleases.filter(({metadata}) => metadata.releaseType === 'asap'), - scheduled: unarchivedReleases.filter(({metadata}) => metadata.releaseType === 'scheduled'), - undecided: unarchivedReleases.filter(({metadata}) => metadata.releaseType === 'undecided'), - } + const releaseTypeReleases = ['asap', 'scheduled', 'undecided'].reduce< + Record + >( + (acc, type) => ({ + ...acc, + [type]: unarchivedReleases.filter(({metadata}) => metadata.releaseType === type), + }), + {}, + ) const range: LayerRange = useMemo(() => { let firstIndex = -1 @@ -100,63 +110,48 @@ export function GlobalPerspectiveMenu(): JSX.Element { firstIndex = 0 // } - if (currentGlobalBundle._id === 'published') { + if (currentGlobalBundleId === 'published') { lastIndex = 0 } - const immediatelyOffset = 2 - const futureOffset = immediatelyOffset + releaseTypeReleases.asap.length - const neverOffset = futureOffset + releaseTypeReleases.scheduled.length + const {asap, scheduled} = releaseTypeReleases - for (const item of releaseTypeReleases.asap) { - const index = immediatelyOffset + releaseTypeReleases.asap.indexOf(item) + const processReleases = (groupSubsetReleases: ReleaseDocument[], offset: number) => { + groupSubsetReleases.forEach(({_id}, i) => { + const index = offset + i - if (firstIndex === -1) { - // if (!item.hidden) { - firstIndex = index - // } - } + if (firstIndex === -1) { + // if (!item.hidden) { + firstIndex = index + // } + } - if (item._id === currentGlobalBundle._id) { - lastIndex = index - } + if (_id === currentGlobalBundleId) { + lastIndex = index + } + }) } - for (const item of releaseTypeReleases.scheduled) { - const index = futureOffset + releaseTypeReleases.scheduled.indexOf(item) - - if (firstIndex === -1) { - // if (!item.hidden) { - firstIndex = index - // } - } - - if (item._id === currentGlobalBundle._id) { - lastIndex = index - } + const offsets = { + asap: 2, + scheduled: 2 + asap.length, + undecided: 2 + asap.length + scheduled.length, } - for (const item of releaseTypeReleases.undecided) { - const index = neverOffset + releaseTypeReleases.undecided.indexOf(item) + const releaseTypeGroups: ('asap' | 'scheduled' | 'undecided')[] = [ + 'asap', + 'scheduled', + 'undecided', + ] - if (firstIndex === -1) { - // if (!item.hidden) { - firstIndex = index - // } - } + releaseTypeGroups.forEach((type) => processReleases(releaseTypeReleases[type], offsets[type])) - if (item._id === currentGlobalBundle._id) { - lastIndex = index - } + return { + firstIndex, + lastIndex, + offsets, } - - return {firstIndex, lastIndex, immediatelyOffset, futureOffset, neverOffset} - }, [ - currentGlobalBundle._id, - releaseTypeReleases.asap, - releaseTypeReleases.scheduled, - releaseTypeReleases.undecided, - ]) + }, [currentGlobalBundleId, releaseTypeReleases]) const releasesList = useMemo(() => { if (loading) { @@ -179,26 +174,25 @@ export function GlobalPerspectiveMenu(): JSX.Element { <> = range.immediatelyOffset + range.firstIndex < range.offsets.asap && range.lastIndex >= range.offsets.asap ? '' : undefined } paddingX={2} paddingTop={4} - paddingBottom={1} - style={{paddingLeft: 33}} + paddingBottom={2} + style={{paddingLeft: 40}} > - {releaseTypeReleases.asap.map((item, index) => ( ))} @@ -207,25 +201,26 @@ export function GlobalPerspectiveMenu(): JSX.Element { <> = range.futureOffset + range.firstIndex < range.offsets.scheduled && + range.lastIndex >= range.offsets.scheduled ? '' : undefined } paddingX={2} paddingTop={4} - paddingBottom={1} - style={{paddingLeft: 33}} + paddingBottom={2} + style={{paddingLeft: 40}} > - {releaseTypeReleases.scheduled.map((item, index) => ( ))} @@ -235,25 +230,26 @@ export function GlobalPerspectiveMenu(): JSX.Element { <> = range.neverOffset + range.firstIndex < range.offsets.undecided && + range.lastIndex >= range.offsets.undecided ? '' : undefined } paddingX={2} paddingTop={4} - paddingBottom={1} - style={{paddingLeft: 33}} + paddingBottom={2} + style={{paddingLeft: 40}} > - {releaseTypeReleases.undecided.map((item, index) => ( ))} diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx index 283115541d9..ca376032a36 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx @@ -1,5 +1,5 @@ import {EyeOpenIcon} from '@sanity/icons' -// eslint-disable-next-line no-restricted-imports +// eslint-disable-next-line no-restricted-imports -- custom use for MenuItem not supported by ui-components import {Box, Flex, MenuItem, Stack, Text} from '@sanity/ui' import {type MouseEvent, useCallback} from 'react' import {getReleaseTone, RelativeTime, ReleaseAvatar, type ReleaseDocument} from 'sanity' @@ -14,7 +14,7 @@ const Root = styled.div` content: ''; display: block; position: absolute; - left: 14px; + left: 18px; bottom: -4px; width: 5px; height: 4px; @@ -28,7 +28,7 @@ const Root = styled.div` content: ''; display: block; position: absolute; - left: 14px; + left: 18px; top: 0; width: 5px; height: 16.5px; @@ -39,7 +39,7 @@ const Root = styled.div` content: ''; display: block; position: absolute; - left: 14px; + left: 18px; top: 16.5px; bottom: 0; width: 5px; @@ -63,8 +63,8 @@ export function GlobalPerspectiveMenuItem(props: { }) { const {release, rangePosition, toggleable} = props // const {current, replace: replaceVersion, replaceToggle} = usePerspective() - const {currentGlobalBundle, setPerspectiveFromRelease} = usePerspective() - const active = release.name === currentGlobalBundle._id + const {currentGlobalBundle, setPerspectiveFromRelease, setPerspective} = usePerspective() + const active = release._id === currentGlobalBundle._id const first = rangePosition === 'first' const within = rangePosition === 'within' const last = rangePosition === 'last' @@ -74,6 +74,14 @@ export function GlobalPerspectiveMenuItem(props: { event.stopPropagation() }, []) + const handleOnReleaseClick = useCallback( + () => + release._id === 'published' + ? setPerspective('published') + : setPerspectiveFromRelease(release._id), + [release._id, setPerspective, setPerspectiveFromRelease], + ) + return ( - setPerspectiveFromRelease(release._id)} - padding={1} - pressed={active} - > - {/*
*/} + Date: Mon, 28 Oct 2024 00:28:30 +0000 Subject: [PATCH 3/7] fix(releases): resolving issue where animation not applied to published perspective in global menu --- .../src/core/releases/navbar/ReleasesNav.tsx | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx index f748f158c87..a382ebb688c 100644 --- a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx +++ b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx @@ -16,6 +16,20 @@ import {getBundleIdFromReleaseId} from '../util/getBundleIdFromReleaseId' import {getReleaseTone} from '../util/getReleaseTone' import {GlobalPerspectiveMenu} from './GlobalPerspectiveMenu' +const MotionDiv = motion(Box) + +const AnimatedMotionDiv = ({children, ...props}: PropsWithChildren) => ( + + {children} + +) + export function ReleasesNav(): JSX.Element { const activeToolName = useRouterState( useCallback( @@ -50,41 +64,37 @@ export function ReleasesNav(): JSX.Element { const currentGlobalPerspectiveLabel = useMemo(() => { if (!currentGlobalBundle || currentGlobalBundle._id === LATEST._id) return null - if (currentGlobalBundle._id === 'published') { - return ( - - - - - {currentGlobalBundle.metadata?.title} - - - - - ) - } - const releasesIntentLink = ({children, ...intentProps}: PropsWithChildren) => ( - - {children} - - ) + const visibleLabelChildren = () => { + if (currentGlobalBundle._id === 'published') { + return ( + + + + + {currentGlobalBundle.metadata?.title} + + + + + ) + } + + const releasesIntentLink = ({children, ...intentProps}: PropsWithChildren) => ( + + {children} + + ) - const tone = currentGlobalBundle.metadata?.releaseType - ? getReleaseTone(currentGlobalBundle) - : 'default' + const tone = currentGlobalBundle.metadata?.releaseType + ? getReleaseTone(currentGlobalBundle) + : 'default' - return ( - + return ( - - ) + ) + } + + return {visibleLabelChildren()} }, [currentGlobalBundle]) return ( From 56eaa2d511b14f088125979ed30e4bf8d36f5e01 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 28 Oct 2024 00:52:34 +0000 Subject: [PATCH 4/7] refactor(releases): sorting global perspective menu items --- .../releases/navbar/GlobalPerspectiveMenu.tsx | 48 +++++++++++++------ .../sanity/src/core/releases/util/util.ts | 11 +++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index 2d49e39dbae..c9648d831c9 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -1,6 +1,7 @@ import {AddIcon, ChevronDownIcon} from '@sanity/icons' // eslint-disable-next-line no-restricted-imports -- MenuItem requires props, only supported by @sanity/ui import {Box, Button, Flex, Label, Menu, MenuDivider, MenuItem, Spinner} from '@sanity/ui' +import {compareDesc} from 'date-fns' import {useCallback, useMemo, useRef, useState} from 'react' import {type ReleaseDocument} from 'sanity' import {styled} from 'styled-components' @@ -10,6 +11,7 @@ import {useTranslation} from '../../i18n' import {useReleases} from '../../store/release/useReleases' import {ReleaseDetailsDialog} from '../components/dialog/ReleaseDetailsDialog' import {usePerspective} from '../hooks' +import {getPublishDateFromRelease} from '../util/util' import {GlobalPerspectiveMenuItem} from './GlobalPerspectiveMenuItem' const StyledMenu = styled(Menu)` @@ -69,6 +71,24 @@ function getRangePosition( return undefined } +const sortReleaseByPublishAt: (a: ReleaseDocument, b: ReleaseDocument) => number = ( + ARelease, + BRelease, +) => compareDesc(getPublishDateFromRelease(BRelease), getPublishDateFromRelease(ARelease)) + +const sortReleaseByTitle: (a: ReleaseDocument, b: ReleaseDocument) => number = ( + ARelease, + BRelease, +) => ARelease.metadata.title.localeCompare(BRelease.metadata.title) + +const releaseTypeSorting = { + asap: sortReleaseByTitle, + scheduled: sortReleaseByPublishAt, + undecided: sortReleaseByTitle, +} + +const releaseTypeGroups: ('asap' | 'scheduled' | 'undecided')[] = ['asap', 'scheduled', 'undecided'] + export function GlobalPerspectiveMenu(): JSX.Element { const {loading, data: releases} = useReleases() const {currentGlobalBundle} = usePerspective() @@ -92,14 +112,18 @@ export function GlobalPerspectiveMenu(): JSX.Element { [releases], ) - const releaseTypeReleases = ['asap', 'scheduled', 'undecided'].reduce< - Record - >( - (acc, type) => ({ - ...acc, - [type]: unarchivedReleases.filter(({metadata}) => metadata.releaseType === type), - }), - {}, + const releaseTypeReleases = useMemo( + () => + releaseTypeGroups.reduce>( + (acc, type) => ({ + ...acc, + [type]: unarchivedReleases + .filter(({metadata}) => metadata.releaseType === type) + .sort(releaseTypeSorting[type]), + }), + {}, + ), + [unarchivedReleases], ) const range: LayerRange = useMemo(() => { @@ -138,12 +162,6 @@ export function GlobalPerspectiveMenu(): JSX.Element { undecided: 2 + asap.length + scheduled.length, } - const releaseTypeGroups: ('asap' | 'scheduled' | 'undecided')[] = [ - 'asap', - 'scheduled', - 'undecided', - ] - releaseTypeGroups.forEach((type) => processReleases(releaseTypeReleases[type], offsets[type])) return { @@ -265,12 +283,12 @@ export function GlobalPerspectiveMenu(): JSX.Element { ) }, [ + handleCreateBundleClick, loading, range, releaseTypeReleases.asap, releaseTypeReleases.scheduled, releaseTypeReleases.undecided, - handleCreateBundleClick, t, ]) diff --git a/packages/sanity/src/core/releases/util/util.ts b/packages/sanity/src/core/releases/util/util.ts index 26272b7e652..cccca41272f 100644 --- a/packages/sanity/src/core/releases/util/util.ts +++ b/packages/sanity/src/core/releases/util/util.ts @@ -60,3 +60,14 @@ export function getCreateVersionOrigin(documentId: string): VersionOriginTypes { if (isPublishedId(documentId)) return 'published' return 'version' } + +/** @internal */ +export function getPublishDateFromRelease(release: ReleaseDocument): Date { + const dateString = release.publishAt || release.metadata.intendedPublishAt + if (!dateString) { + console.error('No publish date found on release', release) + return new Date() + } + + return new Date(dateString) +} From 8c2164922307e199922f39a615a4dc396721f6ab Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 28 Oct 2024 02:14:08 +0000 Subject: [PATCH 5/7] refactor(releases): further tidy to style logic for release layer indicator --- .../releases/navbar/GlobalPerspectiveMenu.tsx | 262 +++++++----------- .../navbar/GlobalPerspectiveMenuItem.tsx | 75 ++--- .../navbar/PerspectiveLayerIndicator.tsx | 101 +++++++ .../src/core/releases/navbar/ReleasesNav.tsx | 40 +-- 4 files changed, 231 insertions(+), 247 deletions(-) create mode 100644 packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index c9648d831c9..8b7c6a83d45 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -3,7 +3,7 @@ import {AddIcon, ChevronDownIcon} from '@sanity/icons' import {Box, Button, Flex, Label, Menu, MenuDivider, MenuItem, Spinner} from '@sanity/ui' import {compareDesc} from 'date-fns' import {useCallback, useMemo, useRef, useState} from 'react' -import {type ReleaseDocument} from 'sanity' +import {type ReleaseDocument, type ReleaseType} from 'sanity' import {styled} from 'styled-components' import {MenuButton} from '../../../ui-components' @@ -12,9 +12,15 @@ import {useReleases} from '../../store/release/useReleases' import {ReleaseDetailsDialog} from '../components/dialog/ReleaseDetailsDialog' import {usePerspective} from '../hooks' import {getPublishDateFromRelease} from '../util/util' -import {GlobalPerspectiveMenuItem} from './GlobalPerspectiveMenuItem' +import { + getRangePosition, + GlobalPerspectiveMenuItem, + type LayerRange, +} from './GlobalPerspectiveMenuItem' +import {GlobalPerspectiveMenuLabelIndicator} from './PerspectiveLayerIndicator' const StyledMenu = styled(Menu)` + min-width: 200; max-width: 320px; ` @@ -23,71 +29,69 @@ const StyledBox = styled(Box)` max-height: 75vh; ` -const LabelBox = styled(Box)` - position: relative; - - &[data-within-range]:before { - content: ''; - display: block; - position: absolute; - left: 18px; - top: 0; - bottom: -4px; - width: 5px; - background-color: var(--card-border-color); - } -` - -interface LayerRange { - firstIndex: number - lastIndex: number - offsets: { - asap: number - scheduled: number - undecided: number - } +const RELEASE_TYPE_LABELS: Record = { + asap: 'release.type.asap', + scheduled: 'release.type.scheduled', + undecided: 'release.type.undecided', } -function getRangePosition( - range: LayerRange, - index: number, -): 'first' | 'within' | 'last' | undefined { - if (range.firstIndex === range.lastIndex) { - return undefined - } - - if (index === range.firstIndex) { - return 'first' - } +function ReleaseTypeSection({ + releaseType, + releases, + range, +}: { + releaseType: ReleaseType + releases: ReleaseDocument[] + range: LayerRange +}): JSX.Element | null { + const {t} = useTranslation() - if (index === range.lastIndex) { - return 'last' - } + if (releases.length === 0) return null - if (index > range.firstIndex && index < range.lastIndex) { - return 'within' - } + const {firstIndex, lastIndex, offsets} = range + const releaseTypeOffset = offsets[releaseType] - return undefined + return ( + <> + = releaseTypeOffset} + paddingRight={2} + paddingTop={4} + paddingBottom={2} + > + + + {releases.map((release, index) => ( + + ))} + + ) } -const sortReleaseByPublishAt: (a: ReleaseDocument, b: ReleaseDocument) => number = ( - ARelease, - BRelease, -) => compareDesc(getPublishDateFromRelease(BRelease), getPublishDateFromRelease(ARelease)) +type ReleaseTypeSort = (a: ReleaseDocument, b: ReleaseDocument) => number -const sortReleaseByTitle: (a: ReleaseDocument, b: ReleaseDocument) => number = ( - ARelease, - BRelease, -) => ARelease.metadata.title.localeCompare(BRelease.metadata.title) +const sortReleaseByPublishAt: ReleaseTypeSort = (ARelease, BRelease) => + compareDesc(getPublishDateFromRelease(BRelease), getPublishDateFromRelease(ARelease)) -const releaseTypeSorting = { +const sortReleaseByTitle: ReleaseTypeSort = (ARelease, BRelease) => + ARelease.metadata.title.localeCompare(BRelease.metadata.title) + +const releaseTypeSorting: Record = { asap: sortReleaseByTitle, scheduled: sortReleaseByPublishAt, undecided: sortReleaseByTitle, } -const releaseTypeGroups: ('asap' | 'scheduled' | 'undecided')[] = ['asap', 'scheduled', 'undecided'] +const orderedReleaseTypes: ReleaseType[] = ['asap', 'scheduled', 'undecided'] + +const ASAP_RANGE_OFFSET = 2 export function GlobalPerspectiveMenu(): JSX.Element { const {loading, data: releases} = useReleases() @@ -112,16 +116,16 @@ export function GlobalPerspectiveMenu(): JSX.Element { [releases], ) - const releaseTypeReleases = useMemo( + const sortedReleaseTypeReleases = useMemo( () => - releaseTypeGroups.reduce>( - (acc, type) => ({ - ...acc, - [type]: unarchivedReleases - .filter(({metadata}) => metadata.releaseType === type) - .sort(releaseTypeSorting[type]), + orderedReleaseTypes.reduce>( + (ReleaseTypeReleases, releaseType) => ({ + ...ReleaseTypeReleases, + [releaseType]: unarchivedReleases + .filter(({metadata}) => metadata.releaseType === releaseType) + .sort(releaseTypeSorting[releaseType]), }), - {}, + {} as Record, ), [unarchivedReleases], ) @@ -138,11 +142,22 @@ export function GlobalPerspectiveMenu(): JSX.Element { lastIndex = 0 } - const {asap, scheduled} = releaseTypeReleases + const {asap, scheduled} = sortedReleaseTypeReleases + const countAsapReleases = asap.length + const countScheduledReleases = scheduled.length + + const offsets = { + asap: ASAP_RANGE_OFFSET, + scheduled: ASAP_RANGE_OFFSET + countAsapReleases, + undecided: ASAP_RANGE_OFFSET + countAsapReleases + countScheduledReleases, + } + + const adjustIndexForReleaseType = (type: ReleaseType) => { + const groupSubsetReleases = sortedReleaseTypeReleases[type] + const offset = offsets[type] - const processReleases = (groupSubsetReleases: ReleaseDocument[], offset: number) => { - groupSubsetReleases.forEach(({_id}, i) => { - const index = offset + i + groupSubsetReleases.forEach(({_id}, groupReleaseIndex) => { + const index = offset + groupReleaseIndex if (firstIndex === -1) { // if (!item.hidden) { @@ -156,20 +171,14 @@ export function GlobalPerspectiveMenu(): JSX.Element { }) } - const offsets = { - asap: 2, - scheduled: 2 + asap.length, - undecided: 2 + asap.length + scheduled.length, - } - - releaseTypeGroups.forEach((type) => processReleases(releaseTypeReleases[type], offsets[type])) + orderedReleaseTypes.forEach(adjustIndexForReleaseType) return { firstIndex, lastIndex, offsets, } - }, [currentGlobalBundleId, releaseTypeReleases]) + }, [currentGlobalBundleId, sortedReleaseTypeReleases]) const releasesList = useMemo(() => { if (loading) { @@ -181,116 +190,31 @@ export function GlobalPerspectiveMenu(): JSX.Element { } return ( - <> + - {releaseTypeReleases.asap.length > 0 && ( - <> - = range.offsets.asap - ? '' - : undefined - } - paddingX={2} - paddingTop={4} - paddingBottom={2} - style={{paddingLeft: 40}} - > - - - {releaseTypeReleases.asap.map((item, index) => ( - - ))} - - )} - {releaseTypeReleases.scheduled.length > 0 && ( - <> - = range.offsets.scheduled - ? '' - : undefined - } - paddingX={2} - paddingTop={4} - paddingBottom={2} - style={{paddingLeft: 40}} - > - - - {releaseTypeReleases.scheduled.map((item, index) => ( - - ))} - - )} - - {releaseTypeReleases.undecided.length > 0 && ( - <> - = range.offsets.undecided - ? '' - : undefined - } - paddingX={2} - paddingTop={4} - paddingBottom={2} - style={{paddingLeft: 40}} - > - - - {releaseTypeReleases.undecided.map((item, index) => ( - - ))} - - )} + {orderedReleaseTypes.map((releaseType) => ( + + ))} - - + ) - }, [ - handleCreateBundleClick, - loading, - range, - releaseTypeReleases.asap, - releaseTypeReleases.scheduled, - releaseTypeReleases.undecided, - t, - ]) + }, [handleCreateBundleClick, loading, range, sortedReleaseTypeReleases, t]) return ( <> diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx index ca376032a36..414059f7354 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx @@ -3,58 +3,33 @@ import {EyeOpenIcon} from '@sanity/icons' import {Box, Flex, MenuItem, Stack, Text} from '@sanity/ui' import {type MouseEvent, useCallback} from 'react' import {getReleaseTone, RelativeTime, ReleaseAvatar, type ReleaseDocument} from 'sanity' -import {styled} from 'styled-components' import {usePerspective} from '../hooks/usePerspective' +import {GlobalPerspectiveMenuItemIndicator} from './PerspectiveLayerIndicator' -const Root = styled.div` - position: relative; - - &[data-in-range]:not([data-last]):after { - content: ''; - display: block; - position: absolute; - left: 18px; - bottom: -4px; - width: 5px; - height: 4px; - background-color: var(--card-border-color); +export interface LayerRange { + firstIndex: number + lastIndex: number + offsets: { + asap: number + scheduled: number + undecided: number } +} - &[data-in-range] > [data-ui='MenuItem'] { - position: relative; - - &:before { - content: ''; - display: block; - position: absolute; - left: 18px; - top: 0; - width: 5px; - height: 16.5px; - background-color: var(--card-border-color); - } - - &:after { - content: ''; - display: block; - position: absolute; - left: 18px; - top: 16.5px; - bottom: 0; - width: 5px; - background-color: var(--card-border-color); - } - } +export function getRangePosition( + range: LayerRange, + index: number, +): 'first' | 'within' | 'last' | undefined { + const {firstIndex, lastIndex} = range - &[data-first] > [data-ui='MenuItem']:before { - display: none; - } + if (firstIndex === lastIndex) return undefined + if (index === firstIndex) return 'first' + if (index === lastIndex) return 'last' + if (index > firstIndex && index < lastIndex) return 'within' - &[data-last] > [data-ui='MenuItem']:after { - display: none; - } -` + return undefined +} export function GlobalPerspectiveMenuItem(props: { release: ReleaseDocument @@ -83,13 +58,7 @@ export function GlobalPerspectiveMenuItem(props: { ) return ( - + - + ) } diff --git a/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx b/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx new file mode 100644 index 00000000000..8a6bdc354b0 --- /dev/null +++ b/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx @@ -0,0 +1,101 @@ +import {Box} from '@sanity/ui' +import {css, styled} from 'styled-components' + +const INDICATOR_LEFT_OFFSET = 18 +const INDICATOR_WIDTH = 5 +const INDICATOR_COLOR_VAR_NAME = '--card-border-color' +const INDICATOR_BOTTOM_OFFSET = 4 + +export const GlobalPerspectiveMenuItemIndicator = styled.div<{ + $inRange: boolean + $last: boolean + $first: boolean +}>( + ({$inRange, $last, $first}) => css` + position: relative; + + --indicator-left: ${INDICATOR_LEFT_OFFSET}px; + --indicator-width: ${INDICATOR_WIDTH}px; + --indicator-color: var(${INDICATOR_COLOR_VAR_NAME}); + --indicator-bottom: ${INDICATOR_BOTTOM_OFFSET}px; + + --indicator-in-range-height: 16.5px; + + ${$inRange && + !$last && + css` + &:after { + content: ''; + display: block; + position: absolute; + left: var(--indicator-left); + bottom: -var(--indicator-bottom); + width: var(--indicator-width); + height: var(--indicator-bottom); + background-color: var(--indicator-color); + } + `} + + ${$inRange && + css` + > [data-ui='MenuItem'] { + position: relative; + + &:before, + &:after { + content: ''; + display: block; + position: absolute; + left: var(--indicator-left); + width: var(--indicator-width); + background-color: var(--indicator-color); + } + + &:before { + top: 0; + height: var(--indicator-in-range-height); + } + + &:after { + top: var(--indicator-in-range-height); + bottom: 0; + } + } + `} + + ${$first && + css` + > [data-ui='MenuItem']:before { + display: none; + } + `} + + ${$last && + css` + > [data-ui='MenuItem']:after { + display: none; + } + `} + `, +) + +export const GlobalPerspectiveMenuLabelIndicator = styled(Box)<{$withinRange: boolean}>( + ({$withinRange}) => css` + position: relative; + padding-left: 40px; + + ${$withinRange && + css` + &:before { + content: ''; + display: block; + position: absolute; + left: ${INDICATOR_LEFT_OFFSET}px; + top: 0; + bottom: -${INDICATOR_BOTTOM_OFFSET}px; + width: ${INDICATOR_WIDTH}px; + background-color: var(${INDICATOR_COLOR_VAR_NAME}); + } + `} + `, +) diff --git a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx index a382ebb688c..a0443eadb62 100644 --- a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx +++ b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx @@ -66,18 +66,21 @@ export function ReleasesNav(): JSX.Element { if (!currentGlobalBundle || currentGlobalBundle._id === LATEST._id) return null const visibleLabelChildren = () => { + const labelContent = ( + + + + + + + {currentGlobalBundle.metadata?.title} + + + + ) + if (currentGlobalBundle._id === 'published') { - return ( - - - - - {currentGlobalBundle.metadata?.title} - - - - - ) + return {labelContent} } const releasesIntentLink = ({children, ...intentProps}: PropsWithChildren) => ( @@ -90,10 +93,6 @@ export function ReleasesNav(): JSX.Element { ) - const tone = currentGlobalBundle.metadata?.releaseType - ? getReleaseTone(currentGlobalBundle) - : 'default' - return ( ) } From 718164aa11a36bb1bcf62ac1e06afb1f5175a400 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 28 Oct 2024 02:28:18 +0000 Subject: [PATCH 6/7] refactor(releases): minor final tidy to global perspective menu; creating release type section component --- .../releases/navbar/GlobalPerspectiveMenu.tsx | 57 ++----------------- .../navbar/GlobalPerspectiveMenuItem.tsx | 9 ++- .../navbar/ReleaseTypeMenuSection.tsx | 55 ++++++++++++++++++ .../src/core/releases/navbar/ReleasesNav.tsx | 6 +- 4 files changed, 66 insertions(+), 61 deletions(-) create mode 100644 packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index 8b7c6a83d45..59fec5890ed 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -1,6 +1,6 @@ import {AddIcon, ChevronDownIcon} from '@sanity/icons' // eslint-disable-next-line no-restricted-imports -- MenuItem requires props, only supported by @sanity/ui -import {Box, Button, Flex, Label, Menu, MenuDivider, MenuItem, Spinner} from '@sanity/ui' +import {Box, Button, Flex, Menu, MenuDivider, MenuItem, Spinner} from '@sanity/ui' import {compareDesc} from 'date-fns' import {useCallback, useMemo, useRef, useState} from 'react' import {type ReleaseDocument, type ReleaseType} from 'sanity' @@ -17,10 +17,12 @@ import { GlobalPerspectiveMenuItem, type LayerRange, } from './GlobalPerspectiveMenuItem' -import {GlobalPerspectiveMenuLabelIndicator} from './PerspectiveLayerIndicator' +import {ReleaseTypeSection} from './ReleaseTypeMenuSection' + +type ReleaseTypeSort = (a: ReleaseDocument, b: ReleaseDocument) => number const StyledMenu = styled(Menu)` - min-width: 200; + min-width: 200px; max-width: 320px; ` @@ -29,57 +31,8 @@ const StyledBox = styled(Box)` max-height: 75vh; ` -const RELEASE_TYPE_LABELS: Record = { - asap: 'release.type.asap', - scheduled: 'release.type.scheduled', - undecided: 'release.type.undecided', -} - -function ReleaseTypeSection({ - releaseType, - releases, - range, -}: { - releaseType: ReleaseType - releases: ReleaseDocument[] - range: LayerRange -}): JSX.Element | null { - const {t} = useTranslation() - - if (releases.length === 0) return null - - const {firstIndex, lastIndex, offsets} = range - const releaseTypeOffset = offsets[releaseType] - - return ( - <> - = releaseTypeOffset} - paddingRight={2} - paddingTop={4} - paddingBottom={2} - > - - - {releases.map((release, index) => ( - - ))} - - ) -} - -type ReleaseTypeSort = (a: ReleaseDocument, b: ReleaseDocument) => number - const sortReleaseByPublishAt: ReleaseTypeSort = (ARelease, BRelease) => compareDesc(getPublishDateFromRelease(BRelease), getPublishDateFromRelease(ARelease)) - const sortReleaseByTitle: ReleaseTypeSort = (ARelease, BRelease) => ARelease.metadata.title.localeCompare(BRelease.metadata.title) diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx index 414059f7354..4984f16b4cd 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx @@ -17,10 +17,9 @@ export interface LayerRange { } } -export function getRangePosition( - range: LayerRange, - index: number, -): 'first' | 'within' | 'last' | undefined { +type rangePosition = 'first' | 'within' | 'last' | undefined + +export function getRangePosition(range: LayerRange, index: number): rangePosition { const {firstIndex, lastIndex} = range if (firstIndex === lastIndex) return undefined @@ -33,7 +32,7 @@ export function getRangePosition( export function GlobalPerspectiveMenuItem(props: { release: ReleaseDocument - rangePosition: 'first' | 'within' | 'last' | undefined + rangePosition: rangePosition toggleable: boolean }) { const {release, rangePosition, toggleable} = props diff --git a/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx new file mode 100644 index 00000000000..c5651a617bd --- /dev/null +++ b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx @@ -0,0 +1,55 @@ +import {Label} from '@sanity/ui' +import {type ReleaseDocument, type ReleaseType, useTranslation} from 'sanity' + +import { + getRangePosition, + GlobalPerspectiveMenuItem, + type LayerRange, +} from './GlobalPerspectiveMenuItem' +import {GlobalPerspectiveMenuLabelIndicator} from './PerspectiveLayerIndicator' + +const RELEASE_TYPE_LABELS: Record = { + asap: 'release.type.asap', + scheduled: 'release.type.scheduled', + undecided: 'release.type.undecided', +} + +export function ReleaseTypeSection({ + releaseType, + releases, + range, +}: { + releaseType: ReleaseType + releases: ReleaseDocument[] + range: LayerRange +}): JSX.Element | null { + const {t} = useTranslation() + + if (releases.length === 0) return null + + const {firstIndex, lastIndex, offsets} = range + const releaseTypeOffset = offsets[releaseType] + + return ( + <> + = releaseTypeOffset} + paddingRight={2} + paddingTop={4} + paddingBottom={2} + > + + + {releases.map((release, index) => ( + + ))} + + ) +} diff --git a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx index a0443eadb62..3fa0473618f 100644 --- a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx +++ b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx @@ -16,10 +16,8 @@ import {getBundleIdFromReleaseId} from '../util/getBundleIdFromReleaseId' import {getReleaseTone} from '../util/getReleaseTone' import {GlobalPerspectiveMenu} from './GlobalPerspectiveMenu' -const MotionDiv = motion(Box) - const AnimatedMotionDiv = ({children, ...props}: PropsWithChildren) => ( - ) => ( transition={{duration: 0.25, ease: 'easeInOut'}} > {children} - + ) export function ReleasesNav(): JSX.Element { From eb92a80c6464e923828afb43eaf26b2bd70e5b3d Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 28 Oct 2024 02:28:49 +0000 Subject: [PATCH 7/7] feat(releases): release table component accepts column style properties --- .../core/releases/tool/components/Table/Table.tsx | 4 ++-- .../releases/tool/components/Table/TableHeader.tsx | 4 ++-- .../src/core/releases/tool/components/Table/types.ts | 3 +++ .../tool/overview/ReleasesOverviewColumnDefs.tsx | 12 +++--------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/sanity/src/core/releases/tool/components/Table/Table.tsx b/packages/sanity/src/core/releases/tool/components/Table/Table.tsx index 725c8525202..8acb9e68622 100644 --- a/packages/sanity/src/core/releases/tool/components/Table/Table.tsx +++ b/packages/sanity/src/core/releases/tool/components/Table/Table.tsx @@ -189,14 +189,14 @@ const TableInner = ({ }} {...cardRowProps} > - {amalgamatedColumnDefs.map(({cell: Cell, width, id, sorting = false}) => ( + {amalgamatedColumnDefs.map(({cell: Cell, style, width, id, sorting = false}) => ( } cellProps={{ as: 'td', id: String(id), - style: {width: width || undefined}, + style: {...style, width: width || undefined}, }} sorting={sorting} /> diff --git a/packages/sanity/src/core/releases/tool/components/Table/TableHeader.tsx b/packages/sanity/src/core/releases/tool/components/Table/TableHeader.tsx index fe6fba7847d..30208870088 100644 --- a/packages/sanity/src/core/releases/tool/components/Table/TableHeader.tsx +++ b/packages/sanity/src/core/releases/tool/components/Table/TableHeader.tsx @@ -88,13 +88,13 @@ export const TableHeader = ({headers, searchDisabled}: TableHeaderProps) => { )`, }} > - {headers.map(({header: Header, width, id, sorting}) => ( + {headers.map(({header: Header, style, width, id, sorting}) => (
{ }) => React.ReactNode id: keyof TableData | string width: number | null + style?: CSSProperties sorting?: boolean sortTransform?: (value: TableData) => number } diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx index 0a573ca3035..65e2f71935a 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx @@ -73,7 +73,7 @@ const ReleaseNameCell: Column['cell'] = ({cellProps, datum: releas const pinButtonIcon = isReleasePinned ? PinFilledIcon : PinIcon return ( - + ( - + ),