Skip to content

Commit

Permalink
fix(core): add permission limitation for create release in document p…
Browse files Browse the repository at this point in the history
…icker
  • Loading branch information
RitaDias committed Feb 14, 2025
1 parent cb931ad commit 3524672
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {AddIcon, CalendarIcon, CopyIcon, TrashIcon} from '@sanity/icons'
import {Menu, MenuDivider, Spinner, Stack} from '@sanity/ui'
import {memo} from 'react'
import {memo, useEffect, useState} from 'react'
import {IntentLink} from 'sanity/router'
import {styled} from 'styled-components'

Expand All @@ -10,6 +10,9 @@ import {useTranslation} from '../../../../i18n/hooks/useTranslation'
import {isPublishedId} from '../../../../util/draftUtils'
import {useReleasesUpsell} from '../../../contexts/upsell/useReleasesUpsell'
import {type ReleaseDocument} from '../../../store/types'
import {useReleaseOperations} from '../../../store/useReleaseOperations'
import {useReleasePermissions} from '../../../store/useReleasePermissions'
import {DEFAULT_RELEASE} from '../../../util/const'
import {isReleaseScheduledOrScheduling} from '../../../util/util'
import {VersionContextMenuItem} from './VersionContextMenuItem'

Expand Down Expand Up @@ -50,8 +53,16 @@ export const VersionContextMenu = memo(function VersionContextMenu(props: {
value: release,
}))

const {checkWithPermissionGuard} = useReleasePermissions()
const {createRelease} = useReleaseOperations()
const [hasCreatePermission, setHasCreatePermission] = useState<boolean | null>(null)

const releaseId = isVersion ? fromRelease : documentId

useEffect(() => {
checkWithPermissionGuard(createRelease, DEFAULT_RELEASE).then(setHasCreatePermission)
}, [checkWithPermissionGuard, createRelease])

return (
<>
<Menu>
Expand All @@ -71,7 +82,8 @@ export const VersionContextMenu = memo(function VersionContextMenu(props: {
icon={CopyIcon}
popover={{placement: 'right-start'}}
text={t('release.action.copy-to')}
disabled={disabled}
disabled={disabled || !hasCreatePermission}
tooltipProps={{content: !hasCreatePermission && t('release.action.permission.error')}}
>
<ReleasesList key={fromRelease} space={1}>
{optionsReleaseList.map((option) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'
import {describe, expect, it, vi} from 'vitest'

import {createTestProvider} from '../../../../../../../test/testUtils/TestProvider'
import {
mockUseReleasePermissions,
useReleasePermissionsMockReturn,
} from '../../../../store/__tests__/__mocks/useReleasePermissions.mock'
import {type ReleaseDocument} from '../../../../store/types'
import {VersionContextMenu} from '../VersionContextMenu'

Expand All @@ -13,6 +17,10 @@ vi.mock('sanity/router', async (importOriginal) => ({
},
}))

vi.mock('../../../../store/useReleasePermissions', () => ({
useReleasePermissions: vi.fn(() => useReleasePermissionsMockReturn),
}))

describe('VersionContextMenu', () => {
const mockReleases: ReleaseDocument[] = [
{
Expand Down Expand Up @@ -58,12 +66,23 @@ describe('VersionContextMenu', () => {
}

it('renders the menu items correctly', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})

const wrapper = await createTestProvider()

render(<VersionContextMenu {...defaultProps} />, {wrapper})

expect(screen.getByText('Copy version to')).toBeInTheDocument()
fireEvent.click(screen.getByText('Copy version to'))

await waitFor(() => {
expect(screen.getByText('Copy version to')).not.toBeDisabled()
})

await act(() => {
fireEvent.click(screen.getByText('Copy version to'))
})
await waitFor(() => {
expect(screen.getByText('New Release')).toBeInTheDocument()
expect(screen.getByText('Release 1')).toBeInTheDocument()
Expand All @@ -72,18 +91,33 @@ describe('VersionContextMenu', () => {
})

it('calls onCreateRelease when "New release" is clicked', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})
const wrapper = await createTestProvider()

render(<VersionContextMenu {...defaultProps} />, {wrapper})

fireEvent.click(screen.getByText('Copy version to'))
expect(screen.getByText('Copy version to')).toBeInTheDocument()

await waitFor(() => {
expect(screen.getByText('Copy version to')).not.toBeDisabled()
})

await act(() => {
fireEvent.click(screen.getByText('Copy version to'))
})

await waitFor(() => {
fireEvent.click(screen.getByText('New Release'))
})
expect(defaultProps.onCreateRelease).toHaveBeenCalled()
})

it('hides discard version on published chip', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})
const wrapper = await createTestProvider()
const publishedProps = {
...defaultProps,
Expand All @@ -97,6 +131,9 @@ describe('VersionContextMenu', () => {
})

it('calls onDiscard when "Discard version" is clicked', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})
const wrapper = await createTestProvider()

render(<VersionContextMenu {...defaultProps} />, {wrapper})
Expand All @@ -108,23 +145,47 @@ describe('VersionContextMenu', () => {
})

it('calls onCreateRelease when a "new release" is clicked', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})
const wrapper = await createTestProvider()

render(<VersionContextMenu {...defaultProps} />, {wrapper})

fireEvent.click(screen.getByText('Copy version to'))
expect(screen.getByText('Copy version to')).toBeInTheDocument()

await waitFor(() => {
expect(screen.getByText('Copy version to')).not.toBeDisabled()
})

await act(() => {
fireEvent.click(screen.getByText('Copy version to'))
})

await waitFor(() => {
fireEvent.click(screen.getByText('New Release'))
})
expect(defaultProps.onCreateRelease).toHaveBeenCalled()
})

it('calls onCreateVersion when a release is clicked and sets the perspective to the release', async () => {
mockUseReleasePermissions.mockReturnValue({
checkWithPermissionGuard: async () => true,
})
const wrapper = await createTestProvider()

render(<VersionContextMenu {...defaultProps} />, {wrapper})

fireEvent.click(screen.getByText('Copy version to'))
expect(screen.getByText('Copy version to')).toBeInTheDocument()

await waitFor(() => {
expect(screen.getByText('Copy version to')).not.toBeDisabled()
})

await act(() => {
fireEvent.click(screen.getByText('Copy version to'))
})

await waitFor(() => {
fireEvent.click(screen.getByText('Release 2'))
})
Expand Down
32 changes: 29 additions & 3 deletions packages/sanity/src/ui-components/menuGroup/MenuGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable no-restricted-imports */
import {MenuGroup as UIMenuGroup, type MenuGroupProps as UIMenuGroupProps} from '@sanity/ui'
import {type HTMLProps} from 'react'
import {type HTMLProps, useCallback} from 'react'

import {
ConditionalWrapper,
type ConditionalWrapperRenderWrapperCallback,
} from '../conditionalWrapper/ConditionalWrapper'
import {Tooltip, type TooltipProps} from '../tooltip/Tooltip'

/** @internal */
export type MenuGroupProps = Pick<UIMenuGroupProps, 'icon' | 'popover' | 'text' | 'tone'>
Expand All @@ -12,7 +18,27 @@ export type MenuGroupProps = Pick<UIMenuGroupProps, 'icon' | 'popover' | 'text'
*/
export const MenuGroup = (
props: MenuGroupProps &
Omit<HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'tabIndex' | 'popover'>,
Omit<HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'tabIndex' | 'popover'> & {
tooltipProps?: TooltipProps | null
},
) => {
return <UIMenuGroup {...props} fontSize={1} padding={3} />
const {tooltipProps} = props

const renderWrapper = useCallback<ConditionalWrapperRenderWrapperCallback>(
(children) => {
return (
<Tooltip content={tooltipProps?.content} portal {...tooltipProps}>
{/* This div is needed to make the tooltip work in disabled menu items */}
<div>{children}</div>
</Tooltip>
)
},
[tooltipProps],
)

return (
<ConditionalWrapper condition={!!tooltipProps} wrapper={renderWrapper}>
<UIMenuGroup {...props} fontSize={1} padding={3} />
</ConditionalWrapper>
)
}

0 comments on commit 3524672

Please sign in to comment.