From 20126270af1d3a151509ba6e7a9c5ccf9be0409e Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Fri, 16 Aug 2024 13:03:07 +0100 Subject: [PATCH 1/5] feat(releases, core): remote deleted version documents will show as banner instead of toast warning --- .../core/bundles/components/BundlesMenu.tsx | 170 +++++++++++ .../components/__tests__/BundlesMenu.test.tsx | 286 ++++++++++++++++++ .../src/core/bundles/components/index.ts | 2 +- .../sanity/src/core/i18n/bundles/studio.ts | 5 +- packages/sanity/src/core/index.ts | 2 +- .../detail/__tests__/ReleaseReview.test.tsx | 4 +- .../detail/__tests__/ReleaseSummary.test.tsx | 4 +- .../perspective/GlobalPerspectiveMenu.tsx | 38 ++- .../document/documentPanel/DocumentPanel.tsx | 45 ++- .../banners/DeletedDocumentBanner.tsx | 46 --- .../banners/DeletedDocumentBanners.tsx | 109 +++++++ .../document/documentPanel/banners/index.ts | 2 +- .../header/DocumentHeaderTitle.tsx | 8 +- .../header/DocumentPanelHeader.tsx | 4 +- .../perspective/DocumentPerspectiveMenu.tsx | 18 +- 15 files changed, 636 insertions(+), 107 deletions(-) create mode 100644 packages/sanity/src/core/bundles/components/BundlesMenu.tsx create mode 100644 packages/sanity/src/core/bundles/components/__tests__/BundlesMenu.test.tsx delete mode 100644 packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanner.tsx create mode 100644 packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx diff --git a/packages/sanity/src/core/bundles/components/BundlesMenu.tsx b/packages/sanity/src/core/bundles/components/BundlesMenu.tsx new file mode 100644 index 00000000000..a9e6bd65772 --- /dev/null +++ b/packages/sanity/src/core/bundles/components/BundlesMenu.tsx @@ -0,0 +1,170 @@ +import {CheckmarkIcon} from '@sanity/icons' +// eslint-disable-next-line no-restricted-imports -- MenuItem requires props, only supported by @sanity/ui +import {Box, Flex, Menu, MenuDivider, MenuItem, Spinner, Text} from '@sanity/ui' +import {memo, type ReactElement, useCallback, useMemo} from 'react' +import {styled} from 'styled-components' + +import {MenuButton, Tooltip} from '../../../ui-components' +import {useTranslation} from '../../i18n' +import {type BundleDocument} from '../../store/bundles/types' +import {useBundles} from '../../store/bundles/useBundles' +import {usePerspective} from '../hooks' +import {LATEST} from '../util/const' +import {isDraftOrPublished} from '../util/util' +import {BundleBadge} from './BundleBadge' + +const StyledMenu = styled(Menu)` + min-width: 200px; +` + +const StyledBox = styled(Box)` + overflow: auto; + max-height: 200px; +` + +interface BundleListProps { + button: ReactElement + bundles: BundleDocument[] | null + loading: boolean + actions?: ReactElement + perspective?: string +} + +/** + * @internal + */ +export const BundlesMenu = memo(function BundlesMenu(props: BundleListProps): ReactElement { + const {bundles, loading, actions, button, perspective} = props + const {deletedBundles} = useBundles() + const {currentGlobalBundle, setPerspective} = usePerspective(perspective) + const {t} = useTranslation() + + const sortedBundlesToDisplay = useMemo(() => { + if (!bundles) return [] + + return bundles + .filter(({slug, archivedAt}) => !isDraftOrPublished(slug) && !archivedAt) + .sort( + ({slug: aSlug}, {slug: bSlug}) => + Number(deletedBundles[aSlug]) - Number(deletedBundles[bSlug]), + ) + }, [bundles, deletedBundles]) + const hasBundles = sortedBundlesToDisplay.length > 0 + + const handleBundleChange = useCallback( + (bundle: Partial) => () => { + if (bundle.slug) { + setPerspective(bundle.slug) + } + }, + [setPerspective], + ) + + const isBundleDeleted = useCallback( + (slug: string) => Boolean(deletedBundles[slug]), + [deletedBundles], + ) + + return ( + <> + + {loading ? ( + + + + ) : ( + <> + + ) : undefined + } + onClick={handleBundleChange(LATEST)} + pressed={false} + text={LATEST.title} + data-testid="latest-menu-item" + /> + {hasBundles && ( + <> + + + {sortedBundlesToDisplay.map((bundle) => ( + + + + + + + + {bundle.title} + + + + {/* + + {bundle.publishAt ? ( + + ) : ( + 'No target date' + )} + + */} + + + + + + + + + + ))} + + + )} + + {actions && ( + <> + + {actions} + + )} + + )} + + } + popover={{ + placement: 'bottom-start', + portal: true, + zOffset: 3000, + }} + /> + + ) +}) diff --git a/packages/sanity/src/core/bundles/components/__tests__/BundlesMenu.test.tsx b/packages/sanity/src/core/bundles/components/__tests__/BundlesMenu.test.tsx new file mode 100644 index 00000000000..fa853d565ea --- /dev/null +++ b/packages/sanity/src/core/bundles/components/__tests__/BundlesMenu.test.tsx @@ -0,0 +1,286 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {fireEvent, render, screen, within} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import {act} from 'react' +import {type BundleDocument, useBundles} from 'sanity' + +import {createTestProvider} from '../../../../../test/testUtils/TestProvider' +import {Button} from '../../../../ui-components' +import {usePerspective} from '../../hooks/usePerspective' +import {LATEST} from '../../util/const' +import {BundlesMenu} from '../BundlesMenu' + +jest.mock('../../hooks/usePerspective', () => ({ + usePerspective: jest.fn().mockReturnValue({ + currentGlobalBundle: {}, + setPerspective: jest.fn(), + }), +})) + +jest.mock('../../util/util', () => ({ + isDraftOrPublished: jest.fn(), +})) + +jest.mock('../../../store/bundles/useBundles', () => ({ + useBundles: jest.fn().mockReturnValue({deletedBundles: {}}), +})) + +const mockUseBundles = useBundles as jest.Mock + +describe('BundlesMenu', () => { + const mockUsePerspective = usePerspective as jest.Mock + const ButtonTest = + + const wrapper = await createTestProvider() + render( + , + { + wrapper, + }, + ) + + fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) + + act(() => { + expect(screen.getByRole('button', {name: 'Actions'})).toBeInTheDocument() + }) + }) + + it('should not show deleted bundles when not included in the list', async () => { + mockUseBundles.mockReturnValue({ + dispatch: jest.fn(), + loading: false, + data: [], + deletedBundles: { + 'mock-deleted-bundle': { + _id: 'mock-deleted-bundle', + _type: 'bundle', + slug: 'mock-deleted-bundle', + title: 'Mock Deleted Bundle', + } as BundleDocument, + }, + }) + const wrapper = await createTestProvider() + render(, { + wrapper, + }) + + fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) + + expect( + within(screen.getByTestId('bundles-list')).queryByText('Mock Deleted Bundle'), + ).not.toBeInTheDocument() + }) + + it('should show deleted bundles that are included in the list', async () => { + mockUseBundles.mockReturnValue({ + dispatch: jest.fn(), + loading: false, + data: [], + deletedBundles: { + 'mock-deleted-bundle': { + _id: 'mock-deleted-bundle', + _type: 'bundle', + slug: 'mock-deleted-bundle', + title: 'Mock Deleted Bundle', + } as BundleDocument, + }, + }) + const wrapper = await createTestProvider() + render( + , + { + wrapper, + }, + ) + + fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) + + const allMenuBundles = within(screen.getByTestId('bundles-list')).getAllByRole('menuitem') + // deleted should be at the end of the bundle list + const [deletedBundle] = allMenuBundles.reverse() + + within(deletedBundle).getByText('Mock Deleted Bundle') + expect(deletedBundle).toBeDisabled() + }) +}) diff --git a/packages/sanity/src/core/bundles/components/index.ts b/packages/sanity/src/core/bundles/components/index.ts index c526de6e9d8..1d65239d954 100644 --- a/packages/sanity/src/core/bundles/components/index.ts +++ b/packages/sanity/src/core/bundles/components/index.ts @@ -1,3 +1,3 @@ export * from './BundleBadge' -export * from './BundleMenu' +export * from './BundlesMenu' export * from './panes/BundleActions' diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 4c046451a89..c1b701c8bbf 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -115,14 +115,15 @@ export const studioLocaleStrings = defineLocalesResources('studio', { /** Text shown in usage dialog for an image asset when there are zero, one or more documents using the *unnamed* image **/ 'asset-source.usage-list.documents-using-image_unnamed_zero': 'No documents are using this image', + /** Label when a release has been deleted by a different user */ + 'banners.deleted-bundle-banner.text': + "The '{{title}}' release has been deleted.", /** Action message to add document to release */ 'bundle.action.add-to-release': 'Add to {{title}}', /** Action message for when document is already in release */ 'bundle.action.already-in-release': 'Already in release {{title}}', /** Action message for creating releases */ 'bundle.action.create': 'Create release', - /** Label when a release has been deleted by a different user */ - 'bundle.deleted-toast-title': "The '{{title}}' release has been deleted", /** Label for tooltip on deleted release */ 'bundle.deleted-tooltip': 'This release has been deleted', /** Title for creating releases dialog */ diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts index 6ed108357e7..660fb77abe6 100644 --- a/packages/sanity/src/core/index.ts +++ b/packages/sanity/src/core/index.ts @@ -1,7 +1,7 @@ export { BundleActions, BundleBadge, - BundleMenu, + BundlesMenu, getBundleSlug, getDocumentIsInPerspective, LATEST, diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx index 45e29451e0b..f876aecaf8a 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseReview.test.tsx @@ -131,8 +131,8 @@ jest.mock('sanity/router', () => ({ }), })) -jest.mock('../../../../bundles/components/BundleMenu', () => ({ - BundleMenu: () =>
BundleMenu
, +jest.mock('../../../../bundles/components/BundlesMenu', () => ({ + BundlesMenu: () =>
BundlesMenu
, })) jest.mock('../../../../preview/useObserveDocument', () => { diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx index 34148f7b2c6..b740ca92459 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseSummary.test.tsx @@ -23,8 +23,8 @@ jest.mock('../../../../user-color', () => ({ useUserColor: jest.fn().mockReturnValue('red'), })) -jest.mock('../../../../bundles/components/BundleMenu', () => ({ - BundleMenu: () =>
BundleMenu
, +jest.mock('../../../../bundles/components/BundlesMenu', () => ({ + BundlesMenu: () =>
BundlesMenu
, })) const timeNow = new Date() diff --git a/packages/sanity/src/core/studio/components/navbar/perspective/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/studio/components/navbar/perspective/GlobalPerspectiveMenu.tsx index dbafa590a71..8ee36b4335b 100644 --- a/packages/sanity/src/core/studio/components/navbar/perspective/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/studio/components/navbar/perspective/GlobalPerspectiveMenu.tsx @@ -1,10 +1,12 @@ import {AddIcon} from '@sanity/icons' -import {Button, MenuItem} from '@sanity/ui' +// eslint-disable-next-line no-restricted-imports -- Bundle Button requires more fine-grained styling than studio button +import {Button} from '@sanity/ui' import {useCallback, useMemo, useState} from 'react' import {useTranslation} from 'sanity' +import {MenuItem} from '../../../../../ui-components' import {BundleBadge} from '../../../../bundles/components/BundleBadge' -import {BundleMenu} from '../../../../bundles/components/BundleMenu' +import {BundlesMenu} from '../../../../bundles/components/BundlesMenu' import {BundleDetailsDialog} from '../../../../bundles/components/dialog/BundleDetailsDialog' import {usePerspective} from '../../../../bundles/hooks/usePerspective' import {useBundles} from '../../../../store/bundles' @@ -32,23 +34,29 @@ export function GlobalPerspectiveMenu(): JSX.Element { [bundles, deletedBundles], ) + const bundleMenuButton = useMemo( + () => ( + + ), + [hue, icon, title], + ) + + const bundleMenuActions = useMemo( + () => ( + + ), + [handleCreateBundleClick, t], + ) + return ( <> - - - - } + - } + actions={bundleMenuActions} /> {createBundleDialogOpen && ( diff --git a/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx b/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx index ebc889cf801..eece6a48f2c 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx @@ -1,6 +1,6 @@ import {BoundaryElementProvider, Box, Flex, PortalProvider, usePortal} from '@sanity/ui' import {createElement, useEffect, useMemo, useRef, useState} from 'react' -import {ScrollContainer, useTimelineSelector, VirtualizerScrollInstanceProvider} from 'sanity' +import {ScrollContainer, VirtualizerScrollInstanceProvider} from 'sanity' import {css, styled} from 'styled-components' import {PaneContent, usePane, usePaneLayout} from '../../../components' @@ -9,7 +9,7 @@ import {DocumentInspectorPanel} from '../documentInspector' import {InspectDialog} from '../inspectDialog' import {useDocumentPane} from '../useDocumentPane' import { - DeletedDocumentBanner, + DeletedDocumentBanners, DeprecatedDocumentTypeBanner, PermissionCheckBanner, ReferenceChangedBanner, @@ -49,7 +49,6 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) { activeViewId, displayed, documentId, - documentType, editState, inspector, value, @@ -58,10 +57,6 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) { schemaType, permissions, isPermissionsLoading, - isDeleting, - isDeleted, - timelineStore, - onChange, } = useDocumentPane() const {collapsed: layoutCollapsed} = usePaneLayout() const {collapsed} = usePane() @@ -112,11 +107,6 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) { [activeView, displayed, documentId, editState?.draft, editState?.published, schemaType, value], ) - const lastNonDeletedRevId = useTimelineSelector( - timelineStore, - (state) => state.lastNonDeletedRevId, - ) - // Scroll to top as `documentId` changes useEffect(() => { if (!documentScrollElement?.scrollTo) return @@ -136,6 +126,22 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) { const showInspector = Boolean(!collapsed && inspector) + const banners = useMemo(() => { + if (activeView.type !== 'form' || isPermissionsLoading || !ready) return null + + return ( + <> + + + + + + ) + }, [activeView.type, isPermissionsLoading, permissions?.granted, ready, requiredPermission]) + return ( @@ -150,20 +156,7 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) { scrollElement={documentScrollElement} containerElement={formContainerElement} > - {activeView.type === 'form' && !isPermissionsLoading && ready && ( - <> - - {!isDeleting && isDeleted && ( - - )} - - - - )} - + {banners} { - if (revisionId) { - restore.execute(revisionId) - navigateIntent('edit', {id: documentId, type: documentType}) - } - }, [documentId, documentType, navigateIntent, restore, revisionId]) - const {t} = useTranslation(structureLocaleNamespace) - - return ( - - {t('banners.deleted-document-banner.text')} - - } - data-testid="deleted-document-banner" - icon={ReadOnlyIcon} - /> - ) -} diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx new file mode 100644 index 00000000000..81a5b24fd73 --- /dev/null +++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx @@ -0,0 +1,109 @@ +import {DocumentRemoveIcon, ReadOnlyIcon} from '@sanity/icons' +import {Text} from '@sanity/ui' +import {useCallback, useEffect, useState} from 'react' +import { + type BundleDocument, + LATEST, + Translate, + useBundles, + useDocumentOperation, + usePerspective, + useTimelineSelector, + useTranslation, +} from 'sanity' +import {useRouter} from 'sanity/router' + +import {structureLocaleNamespace} from '../../../../i18n' +import {useDocumentPane} from '../../useDocumentPane' +import {Banner} from './Banner' + +const useIsLocaleBundleDeleted = () => { + const {currentGlobalBundle} = usePerspective() + const {data: bundles, deletedBundles} = useBundles() + const {slug: currentGlobalBundleSlug} = currentGlobalBundle + const [checkedOutBundleSlug, setCheckedOutBundleSlug] = useState( + currentGlobalBundleSlug, + ) + + useEffect(() => { + if (currentGlobalBundleSlug !== LATEST.slug) { + setCheckedOutBundleSlug(currentGlobalBundleSlug) + } + }, [currentGlobalBundleSlug, setCheckedOutBundleSlug]) + + if (!checkedOutBundleSlug || !Object.keys(deletedBundles).length || !bundles?.length) { + return null + } + return deletedBundles[checkedOutBundleSlug] +} + +export function DeletedDocumentBanners() { + const {isDeleted, isDeleting} = useDocumentPane() + const deletedCheckedOutBundle = useIsLocaleBundleDeleted() + + if (deletedCheckedOutBundle) + return + + if (isDeleted && !isDeleting) return +} + +function DeletedDocumentBanner() { + const {documentId, documentType, timelineStore} = useDocumentPane() + const {restore} = useDocumentOperation(documentId, documentType) + const {navigateIntent} = useRouter() + const lastNonDeletedRevId = useTimelineSelector( + timelineStore, + (state) => state.lastNonDeletedRevId, + ) + const handleRestore = useCallback(() => { + if (lastNonDeletedRevId) { + restore.execute(lastNonDeletedRevId) + navigateIntent('edit', {id: documentId, type: documentType}) + } + }, [documentId, documentType, navigateIntent, restore, lastNonDeletedRevId]) + + const {t} = useTranslation(structureLocaleNamespace) + + return ( + + {t('banners.deleted-document-banner.text')} + + } + data-testid="deleted-document-banner" + icon={ReadOnlyIcon} + /> + ) +} + +const DeletedBundleBanner = ({deletedBundle}: {deletedBundle: BundleDocument}) => { + const {t} = useTranslation() + + const {title: deletedBundleTitle} = deletedBundle + + return ( + + + + } + data-testid="deleted-bundle-banner" + icon={DocumentRemoveIcon} + /> + ) +} diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/index.ts b/packages/sanity/src/structure/panes/document/documentPanel/banners/index.ts index 10f706b9937..ad88037494c 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/banners/index.ts +++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/index.ts @@ -1,4 +1,4 @@ -export * from './DeletedDocumentBanner' +export * from './DeletedDocumentBanners' export * from './DeprecatedDocumentTypeBanner' export * from './PermissionCheckBanner' export * from './ReferenceChangedBanner' diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx index 9a48a11e5ff..6e05c51324e 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx @@ -1,14 +1,14 @@ import {DocumentIcon} from '@sanity/icons' import {Flex, Text} from '@sanity/ui' -import {createElement, type ReactElement} from 'react' +import {createElement, memo, type ReactElement} from 'react' import {unstable_useValuePreview as useValuePreview, useTranslation} from 'sanity' import {structureLocaleNamespace} from '../../../../i18n' import {useDocumentPane} from '../../useDocumentPane' import {DocumentPerspectiveMenu} from './perspective/DocumentPerspectiveMenu' -export function DocumentHeaderTitle(): ReactElement { - const {documentId, connectionState, schemaType, title, value: documentValue} = useDocumentPane() +export const DocumentHeaderTitle = memo(function DocumentHeaderTitle(): ReactElement { + const {connectionState, schemaType, title, value: documentValue} = useDocumentPane() const subscribed = Boolean(documentValue) && connectionState !== 'connecting' const {error, value} = useValuePreview({ @@ -63,4 +63,4 @@ export function DocumentHeaderTitle(): ReactElement { ) -} +}) diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx index 9f19229d337..f65286dfd19 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx @@ -43,6 +43,8 @@ export interface DocumentPanelHeaderProps { menuItems: PaneMenuItem[] } +const documentPaneHeaderTitle = + export const DocumentPanelHeader = memo( forwardRef(function DocumentPanelHeader( _props: DocumentPanelHeaderProps, @@ -138,7 +140,7 @@ export const DocumentPanelHeader = memo( border ref={ref} loading={connectionState === 'connecting'} - title={} + title={documentPaneHeaderTitle} tabs={showTabs && } tabIndex={tabIndex} backButton={ diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx index e853a10326c..2db91805663 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx @@ -1,7 +1,8 @@ import {ChevronDownIcon} from '@sanity/icons' +// eslint-disable-next-line no-restricted-imports -- Bundle Button requires more fine-grained styling than studio button import {Box, Button} from '@sanity/ui' -import {useCallback} from 'react' -import {BundleBadge, BundleMenu, usePerspective} from 'sanity' +import {memo, useCallback, useMemo} from 'react' +import {BundleBadge, BundlesMenu, usePerspective} from 'sanity' import {useRouter} from 'sanity/router' import {styled} from 'styled-components' @@ -12,7 +13,7 @@ const BadgeButton = styled(Button)({ cursor: 'pointer', }) -export function DocumentPerspectiveMenu(): JSX.Element { +export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() { const paneRouter = usePaneRouter() const {currentGlobalBundle} = usePerspective(paneRouter.perspective) @@ -25,6 +26,11 @@ export function DocumentPerspectiveMenu(): JSX.Element { router.navigateIntent('release', {slug}) }, [router, slug]) + const bundlesMenuButton = useMemo( + () => - const mockBundles: BundleDocument[] = [ - { - hue: 'magenta', - _id: 'db76c50e-358b-445c-a57c-8344c588a5d5', - _type: 'bundle', - slug: 'spring-drop', - _rev: '6z08CvvPnPe5pWSKJ5zPRR', - icon: 'heart-filled', - description: 'What a spring drop, allergies galore 🌸', - title: 'Spring Drop', - _updatedAt: '2024-07-02T11:37:51Z', - _createdAt: '2024-07-02T11:37:51Z', - authorId: '', - }, - { - icon: 'drop', - title: 'Autumn Drop', - _type: 'bundle', - hue: 'yellow', - _id: '0e87530e-4378-45ff-9d6f-58207e89f3ed', - _createdAt: '2024-07-02T11:37:06Z', - _rev: '6z08CvvPnPe5pWSKJ5zJiK', - _updatedAt: '2024-07-02T11:37:06Z', - slug: 'autumn-drop', - authorId: '', - }, - { - _createdAt: '2024-07-02T11:36:00Z', - _rev: '22LTUf6tptoEq53N9U5CzE', - icon: 'sun', - description: 'What a summer drop woo hoo! ☀️', - _updatedAt: '2024-07-02T11:36:00Z', - title: 'Summer Drop', - _type: 'bundle', - hue: 'red', - _id: 'f6b2c2cc-1732-4465-bfb3-dd205b5d78e9', - slug: 'summer-drop', - authorId: '', - }, - ] - - beforeEach(() => { - mockUsePerspective.mockClear() - }) - - it('should render loading spinner when loading is true', async () => { - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - expect(screen.getByRole('button', {name: 'Button Test'})).toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByTestId('bundle-menu')).toBeInTheDocument() - expect(screen.getByTestId('spinner')).toBeInTheDocument() - }) - }) - - it('should render latest bundle menu item when bundles are null', async () => { - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByTestId('latest-menu-item')).toBeInTheDocument() - expect(screen.queryByTestId('bundles-list')).not.toBeInTheDocument() - }) - }) - - it('should render latest bundle menu item when bundles are archived', async () => { - const wrapper = await createTestProvider() - const archivedBundles = mockBundles.map((bundle) => ({ - ...bundle, - archivedAt: '2024-07-29T01:49:56.066Z', - })) - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByTestId('latest-menu-item')).toBeInTheDocument() - expect(screen.queryByTestId('bundles-list')).not.toBeInTheDocument() - }) - }) - - it('should render latest bundle menu item as selected when currentGlobalBundle is LATEST', async () => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: LATEST, - setPerspective: jest.fn(), - }) - - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByTestId('latest-menu-item')).toBeInTheDocument() - expect(screen.getByTestId('latest-checkmark-icon')).toBeInTheDocument() - }) - }) - - it('should render bundle as selected when currentGlobalBundle is that bundle', async () => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: mockBundles[0], - setPerspective: jest.fn(), - }) - - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByText(mockBundles[0].title)).toBeInTheDocument() - expect(screen.getByTestId(`${mockBundles[0].slug}-checkmark-icon`)).toBeInTheDocument() - }) - }) - - it('should render bundle menu items when bundles are provided', async () => { - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByText('Spring Drop')).toBeInTheDocument() - expect(screen.getByText('Autumn Drop')).toBeInTheDocument() - expect(screen.getByText('Summer Drop')).toBeInTheDocument() - }) - }) - - it('should call setPerspective when a bundle menu item is clicked', async () => { - const setPerspective = jest.fn() - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: LATEST, - setPerspective, - }) - - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - userEvent.click(screen.getByTestId('bundle-spring-drop')) - expect(setPerspective).toHaveBeenCalledWith('spring-drop') - }) - }) - - it('should render actions when actions prop is provided', async () => { - const actions = - - const wrapper = await createTestProvider() - render( - , - { - wrapper, - }, - ) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - act(() => { - expect(screen.getByRole('button', {name: 'Actions'})).toBeInTheDocument() - }) - }) - - it('should not show deleted bundles when not included in the list', async () => { - mockUseBundles.mockReturnValue({ - dispatch: jest.fn(), - loading: false, - data: [], - deletedBundles: { - 'mock-deleted-bundle': { - _id: 'mock-deleted-bundle', - _type: 'bundle', - slug: 'mock-deleted-bundle', - title: 'Mock Deleted Bundle', - } as BundleDocument, - }, - }) - const wrapper = await createTestProvider() - render(, { - wrapper, - }) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - expect( - within(screen.getByTestId('bundles-list')).queryByText('Mock Deleted Bundle'), - ).not.toBeInTheDocument() - }) - - it('should show deleted bundles that are included in the list', async () => { - mockUseBundles.mockReturnValue({ - dispatch: jest.fn(), - loading: false, - data: [], - deletedBundles: { - 'mock-deleted-bundle': { - _id: 'mock-deleted-bundle', - _type: 'bundle', - slug: 'mock-deleted-bundle', - title: 'Mock Deleted Bundle', - } as BundleDocument, - }, - }) - const wrapper = await createTestProvider() - render( - , - { - wrapper, - }, - ) - - fireEvent.click(screen.getByRole('button', {name: 'Button Test'})) - - const allMenuBundles = within(screen.getByTestId('bundles-list')).getAllByRole('menuitem') - // deleted should be at the end of the bundle list - const [deletedBundle] = allMenuBundles.reverse() - - within(deletedBundle).getByText('Mock Deleted Bundle') - expect(deletedBundle).toBeDisabled() - }) -}) diff --git a/packages/sanity/src/core/bundles/components/dialog/BundleDetailsDialog.tsx b/packages/sanity/src/core/bundles/components/dialog/BundleDetailsDialog.tsx index 0c743c01b51..494c8d0c396 100644 --- a/packages/sanity/src/core/bundles/components/dialog/BundleDetailsDialog.tsx +++ b/packages/sanity/src/core/bundles/components/dialog/BundleDetailsDialog.tsx @@ -1,8 +1,9 @@ import {ArrowRightIcon} from '@sanity/icons' -import {Box, Button, Dialog, Flex, useToast} from '@sanity/ui' +import {Box, Flex, useToast} from '@sanity/ui' import {type FormEvent, useCallback, useState} from 'react' import {useTranslation} from 'sanity' +import {Button, Dialog} from '../../../../ui-components' import {type BundleDocument} from '../../../store/bundles/types' import {useBundleOperations} from '../../../store/bundles/useBundleOperations' import {usePerspective} from '../../hooks/usePerspective' @@ -90,20 +91,14 @@ export function BundleDetailsDialog(props: BundleDetailsDialogProps): JSX.Elemen formAction === 'edit' ? t('bundle.dialog.edit.title') : t('bundle.dialog.create.title') return ( - +
- + - + - ))} + + {COLOR_HUES.map((hue) => ( + + ))} + @@ -101,18 +98,22 @@ export function BundleIconEditorPicker(props: { /> - {Object.entries(icons) - .filter(([key]) => !iconSearchQuery || key.includes(iconSearchQuery.toLowerCase())) - .map(([key, icon]) => ( - diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index c1b701c8bbf..c2a4bdc7598 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -134,6 +134,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', { 'bundle.form.description': 'Description', /** Placeholder for the icon and colour picker */ 'bundle.form.search-icon': 'Search icons', + /** Tooltip label for the icon display */ + 'bundle.form.search-icon-tooltip': 'Select release icon', /** Label for the title form field when creating releases */ 'bundle.form.title': 'Title', diff --git a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx index 891772ca52d..e4abc00dcfe 100644 --- a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx +++ b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx @@ -5,12 +5,12 @@ import { TrashIcon, UnarchiveIcon, } from '@sanity/icons' -import {Button, Menu, MenuButton, Spinner, Text, useToast} from '@sanity/ui' +import {Menu, Spinner, Text, useToast} from '@sanity/ui' import {useState} from 'react' import {useTranslation} from 'sanity' import {useRouter} from 'sanity/router' -import {Dialog, MenuItem} from '../../../../ui-components' +import {Button, Dialog, MenuButton, MenuItem} from '../../../../ui-components' import {BundleDetailsDialog} from '../../../bundles/components/dialog/BundleDetailsDialog' import {type BundleDocument} from '../../../store/bundles/types' import {useBundleOperations} from '../../../store/bundles/useBundleOperations' @@ -80,7 +80,8 @@ export const BundleMenuButton = ({disabled, bundle, documentCount}: BundleMenuBu disabled={bundleMenuDisabled || isPerformingOperation} icon={isPerformingOperation ? Spinner : EllipsisHorizontalIcon} mode="bleed" - padding={2} + style={{padding: 2}} + tooltipProps={{content: t('menu.tooltip')}} aria-label={t('menu.label')} data-testid="release-menu-button" /> diff --git a/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx b/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx index c38c3c3aee5..132b6b25b09 100644 --- a/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx +++ b/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx @@ -1,10 +1,11 @@ import {ErrorOutlineIcon} from '@sanity/icons' import {type PreviewValue} from '@sanity/types' -import {Card, Text, Tooltip} from '@sanity/ui' +import {Card, Text} from '@sanity/ui' import {type ForwardedRef, forwardRef, useMemo} from 'react' import {DocumentPreviewPresence, useDocumentPresence} from 'sanity' import {IntentLink} from 'sanity/router' +import {Tooltip} from '../../../ui-components' import {useTranslation} from '../../i18n' import {SanityDefaultPreview} from '../../preview/components/SanityDefaultPreview' import {getPublishedId} from '../../util/draftUtils' diff --git a/packages/sanity/src/core/releases/components/Table/TableHeader.tsx b/packages/sanity/src/core/releases/components/Table/TableHeader.tsx index 2ebc5c87e96..cdc425c8a61 100644 --- a/packages/sanity/src/core/releases/components/Table/TableHeader.tsx +++ b/packages/sanity/src/core/releases/components/Table/TableHeader.tsx @@ -1,4 +1,5 @@ import {ArrowDownIcon, ArrowUpIcon, SearchIcon} from '@sanity/icons' +// eslint-disable-next-line no-restricted-imports -- Table header using fine-grained styling import {Button, type ButtonProps, Card, Flex, Stack, TextInput} from '@sanity/ui' import {useTableContext} from './TableProvider' diff --git a/packages/sanity/src/core/releases/i18n/resources.ts b/packages/sanity/src/core/releases/i18n/resources.ts index 2e40a931db1..b1e3752c908 100644 --- a/packages/sanity/src/core/releases/i18n/resources.ts +++ b/packages/sanity/src/core/releases/i18n/resources.ts @@ -64,6 +64,8 @@ const releasesLocaleStrings = { /** Label for the release menu */ 'menu.label': 'Release menu', + /** Tooltip for the release menu */ + 'menu.tooltip': 'Actions', /** Text for when no archived releases are found */ 'no-archived-release': 'No archived releases', diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx index bc8d7786965..702d7ac60de 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx @@ -1,9 +1,9 @@ import {AddIcon} from '@sanity/icons' -import {Box, Button, type ButtonMode, Container, Flex, Heading, Stack, Text} from '@sanity/ui' +import {Box, type ButtonMode, Container, Flex, Heading, Stack, Text} from '@sanity/ui' import {isBefore} from 'date-fns' import {type MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState} from 'react' -import {Button as StudioButton} from '../../../../ui-components' +import {Button, Button as StudioButton} from '../../../../ui-components' import {BundleDetailsDialog} from '../../../bundles/components/dialog/BundleDetailsDialog' import {useTranslation} from '../../../i18n' import {type BundleDocument, useBundles} from '../../../store' @@ -134,8 +134,7 @@ export function ReleasesOverview() { icon={AddIcon} disabled={isCreateBundleDialogOpen} onClick={() => setIsCreateBundleDialogOpen(true)} - padding={2} - space={2} + style={{padding: 2}} text={tCore('bundle.action.create')} /> ), diff --git a/packages/sanity/src/ui-components/button/Button.tsx b/packages/sanity/src/ui-components/button/Button.tsx index 0364c06877c..4c9a3d54660 100644 --- a/packages/sanity/src/ui-components/button/Button.tsx +++ b/packages/sanity/src/ui-components/button/Button.tsx @@ -23,6 +23,7 @@ type BaseButtonProps = Pick< | 'tone' | 'type' | 'width' + | 'radius' > & { size?: 'default' | 'large' } From 8118bb19cab452c73df559db3e1bfc5cfea53d13 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 19 Aug 2024 12:03:51 +0100 Subject: [PATCH 3/5] chore(core, releases): removing padding and using ui-component buttons --- .../components/dialog/BundleIconEditorPicker.tsx | 2 +- packages/sanity/src/core/i18n/bundles/studio.ts | 2 ++ .../BundleMenuButton/BundleMenuButton.tsx | 1 - .../releases/components/Table/TableHeader.tsx | 16 ++++++++++------ .../releases/tool/overview/ReleasesOverview.tsx | 1 - .../banners/DeletedDocumentBanners.tsx | 3 +++ .../perspective/DocumentPerspectiveMenu.tsx | 14 +++++++++++--- .../sanity/src/ui-components/button/Button.tsx | 1 - 8 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/sanity/src/core/bundles/components/dialog/BundleIconEditorPicker.tsx b/packages/sanity/src/core/bundles/components/dialog/BundleIconEditorPicker.tsx index 4840d3b1579..4288e580e59 100644 --- a/packages/sanity/src/core/bundles/components/dialog/BundleIconEditorPicker.tsx +++ b/packages/sanity/src/core/bundles/components/dialog/BundleIconEditorPicker.tsx @@ -130,7 +130,7 @@ export function BundleIconEditorPicker(props: { onClick={handleOnPickerOpen} ref={setButton} selected={open} - radius="full" + style={{borderRadius: '50%'}} data-testid="icon-picker-button" > diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index c2a4bdc7598..388d446ec4f 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -138,6 +138,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', { 'bundle.form.search-icon-tooltip': 'Select release icon', /** Label for the title form field when creating releases */ 'bundle.form.title': 'Title', + /** Tooltip for the dropdown to show all versions of document */ + 'bundle.version-list.tooltip': 'See all document versions', /** Action message for navigating to next month */ 'calendar.action.go-to-next-month': 'Go to next month', diff --git a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx index e4abc00dcfe..2497678af8d 100644 --- a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx +++ b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx @@ -80,7 +80,6 @@ export const BundleMenuButton = ({disabled, bundle, documentCount}: BundleMenuBu disabled={bundleMenuDisabled || isPerformingOperation} icon={isPerformingOperation ? Spinner : EllipsisHorizontalIcon} mode="bleed" - style={{padding: 2}} tooltipProps={{content: t('menu.tooltip')}} aria-label={t('menu.label')} data-testid="release-menu-button" diff --git a/packages/sanity/src/core/releases/components/Table/TableHeader.tsx b/packages/sanity/src/core/releases/components/Table/TableHeader.tsx index cdc425c8a61..620eebfa2ad 100644 --- a/packages/sanity/src/core/releases/components/Table/TableHeader.tsx +++ b/packages/sanity/src/core/releases/components/Table/TableHeader.tsx @@ -1,11 +1,17 @@ import {ArrowDownIcon, ArrowUpIcon, SearchIcon} from '@sanity/icons' -// eslint-disable-next-line no-restricted-imports -- Table header using fine-grained styling -import {Button, type ButtonProps, Card, Flex, Stack, TextInput} from '@sanity/ui' +import {Card, Flex, Stack, TextInput} from '@sanity/ui' +import {Button, type ButtonProps} from '../../../../ui-components' import {useTableContext} from './TableProvider' import {type HeaderProps, type TableHeaderProps} from './types' -const SortHeaderButton = ({header, text}: ButtonProps & HeaderProps) => { +const SortHeaderButton = ({ + header, + text, +}: Omit & + HeaderProps & { + text: string + }) => { const {sort, setSortColumn} = useTableContext() const sortIcon = sort?.direction === 'asc' ? ArrowUpIcon : ArrowDownIcon @@ -14,9 +20,7 @@ const SortHeaderButton = ({header, text}: ButtonProps & HeaderProps) => { iconRight={header.sorting && sort?.column === header.id ? sortIcon : undefined} onClick={() => setSortColumn(String(header.id))} mode="bleed" - padding={2} - radius={3} - space={1} + size="default" text={text} /> ) diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx index 702d7ac60de..289b7b26539 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx @@ -134,7 +134,6 @@ export function ReleasesOverview() { icon={AddIcon} disabled={isCreateBundleDialogOpen} onClick={() => setIsCreateBundleDialogOpen(true)} - style={{padding: 2}} text={tCore('bundle.action.create')} /> ), diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx index 81a5b24fd73..0fbde4aeed4 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx @@ -26,6 +26,9 @@ const useIsLocaleBundleDeleted = () => { ) useEffect(() => { + /** + * only named versions other than default (drafts and published) are considered checked-out + */ if (currentGlobalBundleSlug !== LATEST.slug) { setCheckedOutBundleSlug(currentGlobalBundleSlug) } diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx index 2db91805663..d01a829db92 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx @@ -2,10 +2,11 @@ import {ChevronDownIcon} from '@sanity/icons' // eslint-disable-next-line no-restricted-imports -- Bundle Button requires more fine-grained styling than studio button import {Box, Button} from '@sanity/ui' import {memo, useCallback, useMemo} from 'react' -import {BundleBadge, BundlesMenu, usePerspective} from 'sanity' +import {BundleBadge, BundlesMenu, usePerspective, useTranslation} from 'sanity' import {useRouter} from 'sanity/router' import {styled} from 'styled-components' +import {Button as StudioButton} from '../../../../../../ui-components' import {usePaneRouter} from '../../../../../components' import {useDocumentPane} from '../../../useDocumentPane' @@ -15,6 +16,7 @@ const BadgeButton = styled(Button)({ export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() { const paneRouter = usePaneRouter() + const {t} = useTranslation() const {currentGlobalBundle} = usePerspective(paneRouter.perspective) const {documentVersions, existsInBundle} = useDocumentPane() @@ -27,8 +29,14 @@ export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() { }, [router, slug]) const bundlesMenuButton = useMemo( - () =>