diff --git a/packages/sanity/src/core/bundles/util/dummyGetters.ts b/packages/sanity/src/core/bundles/util/dummyGetters.ts index b12cf2d436bc..e6aee37afdb6 100644 --- a/packages/sanity/src/core/bundles/util/dummyGetters.ts +++ b/packages/sanity/src/core/bundles/util/dummyGetters.ts @@ -25,7 +25,7 @@ export async function getAllVersionsOfDocument( return await client.fetch(query, {}, {tag: 'document.list-versions'}).then((documents) => { return documents.map((doc: SanityDocument) => { - const sluggedName = getVersionName(doc._id) + const sluggedName = getBundleSlug(doc._id) const bundle = bundles?.find((b) => b.slug === sluggedName) return { name: speakingurl(sluggedName), @@ -38,7 +38,11 @@ export async function getAllVersionsOfDocument( }) } -export function getVersionName(documentId: string): string { +/** + * @internal + * @hidden + */ +export function getBundleSlug(documentId: string): string { if (documentId.indexOf('.') === -1) return 'Published' const version = documentId.slice(0, documentId.indexOf('.')) return version diff --git a/packages/sanity/src/core/bundles/util/tests/createWrapper.tsx b/packages/sanity/src/core/bundles/util/tests/createWrapper.tsx index 120caff9290b..f190aee7cb20 100644 --- a/packages/sanity/src/core/bundles/util/tests/createWrapper.tsx +++ b/packages/sanity/src/core/bundles/util/tests/createWrapper.tsx @@ -6,6 +6,10 @@ import { type TestProviderOptions, } from '../../../../../test/testUtils/TestProvider' +/** + * @internal + * @hidden + */ export const createWrapper = async (options?: TestProviderOptions) => { const TestProvider = await createTestProvider(options) return function Wrapper({children}: {children: ReactNode}): JSX.Element { diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts index 341df7781cda..06cc369f1b08 100644 --- a/packages/sanity/src/core/index.ts +++ b/packages/sanity/src/core/index.ts @@ -2,7 +2,9 @@ export { BundleActions, BundleBadge, BundleMenu, + createWrapper, getAllVersionsOfDocument, + getBundleSlug, LATEST, usePerspective, } from './bundles' diff --git a/packages/sanity/src/core/releases/plugin/index.ts b/packages/sanity/src/core/releases/plugin/index.ts index 6fb23ab6b6c0..09b5fe5be986 100644 --- a/packages/sanity/src/core/releases/plugin/index.ts +++ b/packages/sanity/src/core/releases/plugin/index.ts @@ -25,6 +25,15 @@ export const releases = definePlugin({ title: 'Releases', component: ReleasesTool, router: route.create('/', [route.create('/:bundleSlug')]), + canHandleIntent: (intent, params) => { + return Boolean(intent === 'release' && params.slug) + }, + getIntentState(intent, params) { + if (intent === 'release') { + return {bundleSlug: params.slug} + } + return null + }, }, ], }) diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.test.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.test.tsx index e6f489789d16..de78fa0d9d99 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.test.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.test.tsx @@ -1,6 +1,11 @@ import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals' import {render, waitFor} from '@testing-library/react' -import {defineConfig, type SanityClient, unstable_useValuePreview as useValuePreview} from 'sanity' +import { + defineConfig, + type SanityClient, + unstable_useValuePreview as useValuePreview, + useBundles, +} from 'sanity' import {useRouter} from 'sanity/router' import {createMockSanityClient} from '../../../../../../test/mocks/mockSanityClient' @@ -31,6 +36,8 @@ jest.mock('sanity', () => { return { ...actual, unstable_useValuePreview: jest.fn(), + useBundles: jest.fn(), + getBundleSlug: jest.fn(() => ''), } }) @@ -39,6 +46,8 @@ jest.mock('../../../../../core/bundles/util/dummyGetters', () => ({ getAllVersionsOfDocument: jest.fn(() => []), })) +const mockUseBundles = useBundles as jest.Mock + describe('DocumentHeaderTitle', () => { const mockUseDocumentPane = useDocumentPane as jest.MockedFunction const mockUseValuePreview = useValuePreview as jest.MockedFunction @@ -59,6 +68,11 @@ describe('DocumentHeaderTitle', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore mockUseRouter.mockReturnValue({stickyParams: {}, state: {}, navigate: jest.fn()}) + mockUseBundles.mockReturnValue({ + data: [], + loading: false, + dispatch: jest.fn(), + }) }) afterEach(() => { diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.test.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.test.tsx new file mode 100644 index 000000000000..875ab948b337 --- /dev/null +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.test.tsx @@ -0,0 +1,164 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {fireEvent, render, screen} from '@testing-library/react' +import { + BundleBadge, + type BundleDocument, + createWrapper, + getAllVersionsOfDocument, + getBundleSlug, + type SanityClient, + useBundles, + useClient, + usePerspective, +} from 'sanity' +import {useRouter} from 'sanity/router' + +import {DocumentPerspectiveMenu} from './DocumentPerspectiveMenu' + +type getBundleSlugType = (documentId: string) => string +type GetAllVersionsOfDocumentType = ( + bundles: BundleDocument[] | null, + client: SanityClient, + documentId: string, +) => Promise[]> + +jest.mock('sanity', () => ({ + useClient: jest.fn(), + usePerspective: jest.fn().mockReturnValue({ + currentGlobalBundle: {}, + setPerspective: jest.fn(), + }), + getAllVersionsOfDocument: jest.fn(() => + Promise.resolve([ + { + name: 'spring-drop', + title: 'Spring Drop', + hue: 'magenta', + icon: 'heart-filled', + }, + ]), + ), + BundleBadge: jest.fn(), + useBundles: jest.fn(), + getBundleSlug: jest.fn(() => ''), +})) + +jest.mock('sanity/router', () => ({ + useRouter: jest.fn().mockReturnValue({ + navigateIntent: jest.fn(), + }), + route: { + create: jest.fn(), + }, + IntentLink: jest.fn(), +})) + +const mockUseClient = useClient as jest.Mock +const mockUseRouter = useRouter as jest.MockedFunction +const navigateIntent = mockUseRouter().navigateIntent as jest.Mock + +const mockUseBundles = useBundles as jest.Mock +const mockUsePerspective = usePerspective as jest.Mock +const mockGetBundleSlug = getBundleSlug as jest.MockedFunction +const mockGetAllVersionsOfDocument = + getAllVersionsOfDocument as jest.MockedFunction +const mockBundleBadge = BundleBadge as jest.Mock + +describe('DocumentPerspectiveMenu', () => { + const mockCurrent: BundleDocument = { + description: 'What a spring drop, allergies galore 🌸', + _updatedAt: '2024-07-12T10:39:32Z', + _rev: 'HdJONGqRccLIid3oECLjYZ', + authorId: 'pzAhBTkNX', + title: 'Spring Drop', + icon: 'heart-filled', + _id: 'db76c50e-358b-445c-a57c-8344c588a5d5', + _type: 'bundle', + slug: 'spring-drop', + hue: 'magenta', + _createdAt: '2024-07-02T11:37:51Z', + } + + beforeEach(() => { + jest.clearAllMocks() + + mockUseClient.mockReturnValue({}) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + + mockBundleBadge.mockImplementation(() =>
"test"
) + + // Mock the data returned by useBundles hook + const mockData: BundleDocument[] = [mockCurrent] + + mockUseBundles.mockReturnValue({ + data: mockData, + loading: false, + dispatch: jest.fn(), + }) + + mockUsePerspective.mockReturnValue({ + currentGlobalBundle: mockCurrent, + setPerspective: jest.fn(), + }) + }) + + it('should render the bundle badge if the document exists in the global bundle', async () => { + // Dummy Getters + mockGetBundleSlug.mockReturnValue('spring-drop') + + mockGetAllVersionsOfDocument.mockImplementationOnce( + (): Promise => + Promise.resolve([ + { + name: 'spring-drop', + title: 'Spring Drop', + hue: 'magenta', + icon: 'heart-filled', + }, + ]), + ) + + const wrapper = await createWrapper() + render(, {wrapper}) + + expect(screen.getByTestId('button-document-release')).toBeInTheDocument() + }) + + it('should not render the bundle badge if the document does not exist in the bundle', async () => { + // Dummy Getters + mockGetBundleSlug.mockReturnValue('no-bundle') + + const wrapper = await createWrapper() + render(, {wrapper}) + + expect(screen.queryByTestId('button-document-release')).toBeNull() + }) + + it('should navigate to the release intent when the bundle badge is clicked', async () => { + // Dummy Getters + mockGetBundleSlug.mockReturnValue('spring-drop') + + mockGetAllVersionsOfDocument.mockImplementationOnce( + (): Promise => + Promise.resolve([ + { + name: 'spring-drop', + title: 'Spring Drop', + hue: 'magenta', + icon: 'heart-filled', + }, + ]), + ) + + const wrapper = await createWrapper() + render(, {wrapper}) + + expect(screen.queryByTestId('button-document-release')).toBeInTheDocument() + fireEvent.click(screen.getByTestId('button-document-release')) + + expect(navigateIntent).toHaveBeenCalledTimes(1) + expect(navigateIntent).toHaveBeenCalledWith('release', {slug: 'spring-drop'}) + }) +}) 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 05cb4d571fd0..53bdee5527c3 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,30 +1,35 @@ import {ChevronDownIcon} from '@sanity/icons' import {Box, Button} from '@sanity/ui' -import {useCallback, useEffect, useState} from 'react' +import {useCallback, useEffect, useMemo, useState} from 'react' import { BundleBadge, type BundleDocument, BundleMenu, DEFAULT_STUDIO_CLIENT_OPTIONS, getAllVersionsOfDocument, - LATEST, + getBundleSlug, + useBundles, useClient, usePerspective, } from 'sanity' +import {useRouter} from 'sanity/router' +import {styled} from 'styled-components' -// FIXME -// eslint-disable-next-line boundaries/element-types -import {useBundles} from '../../../../../../core/store/bundles/useBundles' +const BadgeButton = styled(Button)({ + cursor: 'pointer', +}) export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Element { const {documentId} = props const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) - + const {data} = useBundles() const {currentGlobalBundle} = usePerspective() + const bundles = useMemo(() => data ?? [], [data]) - const {title, hue, icon} = currentGlobalBundle + const existsInBundle = bundles.some((bundle) => bundle.slug === getBundleSlug(documentId)) + const {title, hue, icon, slug} = currentGlobalBundle - const {data: bundles} = useBundles() + const router = useRouter() // TODO MAKE SURE THIS IS HOW WE WANT TO DO THIS const [documentVersions, setDocumentVersions] = useState([]) @@ -43,21 +48,33 @@ export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Elemen fetchVersionsInner() }, [fetchVersions]) + const handleBundleClick = useCallback(() => { + router.navigateIntent('release', {slug}) + }, [router, slug]) + return ( <> - {/* FIXME Version Badge should only show when the current opened document is in a version, RIGHT - NOW IT'S USING THE GLOBAL */} - {currentGlobalBundle && currentGlobalBundle.slug === LATEST.slug && ( - + {currentGlobalBundle && existsInBundle && ( + + + )} {/** TODO IS THIS STILL NEEDED? VS THE PICKER IN STUDIO NAVBAR? */} - - } - bundles={documentVersions} - loading={!documentVersions} - /> - + {documentVersions.length > 0 && ( + + } + bundles={documentVersions} + loading={!documentVersions} + /> + + )} ) }