Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: global perspective picker facelift #7629

Merged
merged 4 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading