Skip to content

Commit

Permalink
feat: global perspective picker facelift (#7629)
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanl17 authored Oct 18, 2024
1 parent 3fb444f commit c80e628
Show file tree
Hide file tree
Showing 15 changed files with 539 additions and 159 deletions.
22 changes: 16 additions & 6 deletions packages/sanity/src/core/config/studio/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,30 @@ export interface LogoProps {
renderDefault: (props: LogoProps) => ReactElement
}

/**
* @internal
* @beta
* An internal API for defining actions in the navbar.
*/
export interface NavbarAction {
interface NavbarActionBase {
icon?: React.ComponentType
location: 'topbar' | 'sidebar'
name: string
}

interface ActionWithCustomRender extends NavbarActionBase {
render: () => ReactElement
}

interface Action extends NavbarActionBase {
onAction: () => void
selected: boolean
title: string
render?: undefined
}

/**
* @internal
* @beta
* An internal API for defining actions in the navbar.
*/
export type NavbarAction = Action | ActionWithCustomRender

/**
* @hidden
* @beta */
Expand Down
2 changes: 2 additions & 0 deletions packages/sanity/src/core/i18n/bundles/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'release.form.type.scheduled': 'At time',
/** Label for the release type 'undecided' */
'release.form.type.undecided': 'Undecided',
/** Tooltip for releases navigation in navbar */
'release.navbar.tooltip': 'Releases',
/** Tooltip for the dropdown to show all versions of document */
'release.version-list.tooltip': 'See all document versions',

Expand Down
8 changes: 7 additions & 1 deletion packages/sanity/src/core/releases/hooks/usePerspective.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export function usePerspective(selectedPerspective?: string): PerspectiveValue {
: LATEST

// TODO: Improve naming; this may not be global.
const currentGlobalBundle = selectedBundle || LATEST
const currentGlobalBundle =
perspective === 'published'
? {
_id: 'published',
title: 'Published',
}
: selectedBundle || LATEST

return {
setPerspective,
Expand Down
180 changes: 180 additions & 0 deletions packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {AddIcon, CheckmarkIcon, ChevronDownIcon} from '@sanity/icons'
// eslint-disable-next-line no-restricted-imports -- MenuItem requires props, only supported by @sanity/ui
import {Box, Button, Flex, Menu, MenuDivider, MenuItem, Spinner, Text} from '@sanity/ui'
import {useCallback, useMemo, useRef, useState} from 'react'
import {styled} from 'styled-components'

import {MenuButton, Tooltip} from '../../../ui-components'
import {useTranslation} from '../../i18n'
import {useBundles} from '../../store/bundles/useBundles'
import {ReleaseDetailsDialog} from '../components/dialog/ReleaseDetailsDialog'
import {usePerspective} from '../hooks'
import {LATEST} from '../util/const'
import {isDraftOrPublished} from '../util/util'

const StyledMenu = styled(Menu)`
min-width: 200px;
`

const StyledBox = styled(Box)`
overflow: auto;
max-height: 200px;
`

export function GlobalPerspectiveMenu(): JSX.Element {
const {deletedBundles, loading, data: bundles} = useBundles()
const {currentGlobalBundle, setPerspective} = usePerspective()
const [createBundleDialogOpen, setCreateBundleDialogOpen] = useState(false)
const styledMenuRef = useRef<HTMLDivElement>(null)

const {t} = useTranslation()

const sortedBundlesToDisplay = useMemo(() => {
if (!bundles) return []

return [...(bundles || []), ...Object.values(deletedBundles)].filter(
({_id, archivedAt}) => !isDraftOrPublished(_id) && !archivedAt,
)
}, [bundles, deletedBundles])
const hasBundles = sortedBundlesToDisplay.length > 0

const handleBundleChange = useCallback(
(bundleId: string) => () => {
setPerspective(bundleId)
},
[setPerspective],
)

const isBundleDeleted = useCallback(
(bundleId: string) => Boolean(deletedBundles[bundleId]),
[deletedBundles],
)

/* create new bundle */
const handleCreateBundleClick = useCallback(() => {
setCreateBundleDialogOpen(true)
}, [])

const handleClose = useCallback(() => {
setCreateBundleDialogOpen(false)
}, [])

const releasesList = useMemo(() => {
if (loading) {
return (
<Flex padding={4} justify="center" data-testid="spinner">
<Spinner muted />
</Flex>
)
}

return (
<>
<MenuItem
iconRight={
currentGlobalBundle._id === LATEST._id ? (
<CheckmarkIcon data-testid="latest-checkmark-icon" />
) : undefined
}
onClick={handleBundleChange(LATEST._id)}
pressed={false}
text={LATEST.title}
data-testid="latest-menu-item"
/>
{hasBundles && (
<>
<MenuDivider />
<StyledBox data-testid="bundles-list">
{sortedBundlesToDisplay.map(({_id, ...bundle}) => (
<MenuItem
key={_id}
onClick={handleBundleChange(_id)}
padding={1}
pressed={false}
disabled={isBundleDeleted(_id)}
data-testid={`bundle-${_id}`}
>
<Tooltip
disabled={!isBundleDeleted(_id)}
content={t('release.deleted-tooltip')}
placement="bottom-start"
>
<Flex>
<Box flex={1} padding={2} style={{minWidth: 100}}>
<Text size={1} weight="medium">
{bundle.title}
</Text>
</Box>
<Box padding={2}>
<Text size={1}>
<CheckmarkIcon
style={{
opacity: currentGlobalBundle._id === _id ? 1 : 0,
}}
data-testid={`${_id}-checkmark-icon`}
/>
</Text>
</Box>
</Flex>
</Tooltip>
</MenuItem>
))}
</StyledBox>
</>
)}

<>
<MenuDivider />
<MenuItem
icon={AddIcon}
onClick={handleCreateBundleClick}
text={t('release.action.create')}
/>
</>
</>
)
}, [
currentGlobalBundle._id,
handleBundleChange,
handleCreateBundleClick,
hasBundles,
isBundleDeleted,
loading,
sortedBundlesToDisplay,
t,
])

return (
<>
<MenuButton
button={
<Button
data-testid="global-perspective-menu-button"
iconRight={ChevronDownIcon}
mode="bleed"
padding={2}
radius="full"
space={2}
/>
}
id="releases-menu"
menu={
<StyledMenu data-testid="release-menu" ref={styledMenuRef}>
{releasesList}
</StyledMenu>
}
popover={{
constrainSize: true,
fallbackPlacements: ['bottom-end'],
placement: 'bottom-end',
portal: true,
tone: 'default',
zOffset: 3000,
}}
/>
{createBundleDialogOpen && (
<ReleaseDetailsDialog onCancel={handleClose} onSubmit={handleClose} origin="structure" />
)}
</>
)
}
113 changes: 113 additions & 0 deletions packages/sanity/src/core/releases/navbar/ReleasesNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {CalendarIcon, CloseIcon} from '@sanity/icons'
// eslint-disable-next-line no-restricted-imports -- Bundle Button requires more fine-grained styling than studio button
import {Box, Button, Card, Flex, Stack, Text} from '@sanity/ui'
import {type PropsWithChildren, useCallback, useMemo} from 'react'
import {useTranslation} from 'react-i18next'
import {LATEST, ToolLink} from 'sanity'
import {IntentLink, useRouterState} from 'sanity/router'

import {Tooltip} from '../../../ui-components'
import {usePerspective} from '../hooks/usePerspective'
import {RELEASES_INTENT, RELEASES_TOOL_NAME} from '../plugin'
import {GlobalPerspectiveMenu} from './GlobalPerspectiveMenu'

export function ReleasesNav(): JSX.Element {
const activeToolName = useRouterState(
useCallback(
(routerState) => (typeof routerState.tool === 'string' ? routerState.tool : undefined),
[],
),
)

const {currentGlobalBundle, setPerspective} = usePerspective()
const {t} = useTranslation()

const handleClearPerspective = () => setPerspective(LATEST._id)

const releasesToolLink = useMemo(
() => (
<Tooltip content={t('release.navbar.tooltip')}>
<Button
as={ToolLink}
name={RELEASES_TOOL_NAME}
data-as="a"
icon={CalendarIcon}
mode="bleed"
padding={2}
radius="full"
selected={activeToolName === RELEASES_TOOL_NAME}
space={2}
/>
</Tooltip>
),
[activeToolName, t],
)

const currentGlobalPerspectiveLabel = useMemo(() => {
if (currentGlobalBundle._id === LATEST._id) return null
if (currentGlobalBundle._id === 'published') {
return (
<Card tone="inherit">
<Flex align="flex-start" gap={0}>
<Stack flex={1} paddingY={2} paddingX={2} space={2}>
<Text size={1} textOverflow="ellipsis" weight="medium">
{currentGlobalBundle.title}
</Text>
</Stack>
</Flex>
</Card>
)
}

const releasesIntentLink = ({children, ...intentProps}: PropsWithChildren) => (
<IntentLink {...intentProps} intent={RELEASES_INTENT} params={{id: currentGlobalBundle._id}}>
{children}
</IntentLink>
)

return (
<Button
as={releasesIntentLink}
data-as="a"
rel="noopener noreferrer"
mode="bleed"
padding={0}
radius="full"
style={{maxWidth: '180px'}}
>
<Flex align="flex-start" gap={0}>
{/* <Box flex="none">
<VersionAvatar icon={current.icon} padding={2} tone={current.tone} />
</Box> */}
<Stack flex={1} paddingY={2} paddingX={2} space={2}>
<Text size={1} textOverflow="ellipsis" weight="medium">
{currentGlobalBundle.title}
</Text>
</Stack>
</Flex>
</Button>
)
}, [currentGlobalBundle._id, currentGlobalBundle.title])

return (
<Card flex="none" border marginRight={1} radius="full" tone="inherit" style={{margin: -1}}>
<Flex gap={0}>
<Box flex="none">{releasesToolLink}</Box>
{currentGlobalPerspectiveLabel}
<GlobalPerspectiveMenu />
{currentGlobalBundle._id !== LATEST._id && (
<div>
<Button
icon={CloseIcon}
mode="bleed"
onClick={handleClearPerspective}
data-testid="clear-perspective-button"
padding={2}
radius="full"
/>
</div>
)}
</Flex>
</Card>
)
}
29 changes: 29 additions & 0 deletions packages/sanity/src/core/releases/navbar/ReleasesStudioNavbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {useMemo} from 'react'

import {type NavbarProps} from '../../config'
import {ReleasesNav} from './ReleasesNav'

export const ReleasesStudioNavbar = (props: NavbarProps) => {
const actions = useMemo(
(): NavbarProps['__internal_actions'] => [
{
location: 'topbar',
name: 'releases-topbar',
render: ReleasesNav,
},
{
location: 'sidebar',
name: 'releases-sidebar',
render: ReleasesNav,
},
...(props?.__internal_actions || []),
],
[props?.__internal_actions],
)

return props.renderDefault({
...props,
// eslint-disable-next-line camelcase
__internal_actions: actions,
})
}
Loading

0 comments on commit c80e628

Please sign in to comment.