diff --git a/dev/studio-e2e-testing/sanity.config.ts b/dev/studio-e2e-testing/sanity.config.ts index 2898d6a3feb..5a9c830981f 100644 --- a/dev/studio-e2e-testing/sanity.config.ts +++ b/dev/studio-e2e-testing/sanity.config.ts @@ -99,4 +99,7 @@ export default defineConfig({ enabled: true, }, }, + announcements: { + enabled: false, + }, }) diff --git a/packages/sanity/src/core/config/configPropertyReducers.ts b/packages/sanity/src/core/config/configPropertyReducers.ts index 82de92a424c..ce9c2d888cf 100644 --- a/packages/sanity/src/core/config/configPropertyReducers.ts +++ b/packages/sanity/src/core/config/configPropertyReducers.ts @@ -497,3 +497,26 @@ export const createFallbackOriginReducer = (config: PluginOptions): string | und return result } + +export const announcementsEnabledReducer = (opts: { + config: PluginOptions + initialValue: boolean +}): boolean => { + const {config, initialValue} = opts + const flattenedConfig = flattenConfig(config, []) + + const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { + const resolver = innerConfig.announcements?.enabled + + if (!resolver && typeof resolver !== 'boolean') return acc + if (typeof resolver === 'boolean') return resolver + + throw new Error( + `Expected \`announcements.enabled\` to be a boolean, but received ${getPrintableType( + resolver, + )}`, + ) + }, initialValue) + + return result +} diff --git a/packages/sanity/src/core/config/prepareConfig.ts b/packages/sanity/src/core/config/prepareConfig.ts index 0903298ceca..7b771f7596c 100644 --- a/packages/sanity/src/core/config/prepareConfig.ts +++ b/packages/sanity/src/core/config/prepareConfig.ts @@ -25,6 +25,7 @@ import {operatorDefinitions} from '../studio/components/navbar/search/definition import {type InitialValueTemplateItem, type Template, type TemplateItem} from '../templates' import {EMPTY_ARRAY, isNonNullable} from '../util' import { + announcementsEnabledReducer, createFallbackOriginReducer, documentActionsReducer, documentBadgesReducer, @@ -663,6 +664,10 @@ function resolveSource({ fallbackStudioOrigin: createFallbackOriginReducer(config), }, }, + + announcements: { + enabled: announcementsEnabledReducer({config, initialValue: true}), + }, } return source diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts index 4852fd8b7d2..8fc8064afd4 100644 --- a/packages/sanity/src/core/config/types.ts +++ b/packages/sanity/src/core/config/types.ts @@ -416,6 +416,13 @@ export interface PluginOptions { * @beta */ onUncaughtError?: (error: Error, errorInfo: ErrorInfo) => void + /** + * @hidden + * @internal + */ + announcements?: { + enabled: boolean + } } /** @internal */ @@ -810,6 +817,13 @@ export interface Source { * @beta */ onUncaughtError?: (error: Error, errorInfo: ErrorInfo) => void + /** + * @hidden + * @internal + */ + announcements?: { + enabled: boolean + } } /** @internal */ diff --git a/packages/sanity/src/core/studio/studioAnnouncements/StudioAnnouncementsProvider.tsx b/packages/sanity/src/core/studio/studioAnnouncements/StudioAnnouncementsProvider.tsx index 0d72632078d..49d6f82cd1e 100644 --- a/packages/sanity/src/core/studio/studioAnnouncements/StudioAnnouncementsProvider.tsx +++ b/packages/sanity/src/core/studio/studioAnnouncements/StudioAnnouncementsProvider.tsx @@ -6,6 +6,7 @@ import {catchError, combineLatest, map, type Observable, startWith} from 'rxjs' import {StudioAnnouncementContext} from 'sanity/_singletons' import {useClient} from '../../hooks/useClient' +import {useSource} from '../../studio/source' import {useWorkspace} from '../../studio/workspace' import {SANITY_VERSION} from '../../version' import { @@ -28,11 +29,7 @@ interface StudioAnnouncementsProviderProps { } const CLIENT_OPTIONS = {apiVersion: 'v2024-09-19'} -/** - * @internal - * @hidden - */ -export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProviderProps) { +function StudioAnnouncementsProviderInner({children}: StudioAnnouncementsProviderProps) { const telemetry = useTelemetry() const [dialogMode, setDialogMode] = useState(null) const [isCardDismissed, setIsCardDismissed] = useState(false) @@ -159,3 +156,16 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi ) } + +/** + * @internal + * @hidden + */ +export function StudioAnnouncementsProvider(props: StudioAnnouncementsProviderProps) { + const source = useSource() + + if (source.announcements?.enabled) { + return + } + return props.children +} diff --git a/packages/sanity/src/core/studio/studioAnnouncements/__tests__/StudioAnnouncementsProvider.test.tsx b/packages/sanity/src/core/studio/studioAnnouncements/__tests__/StudioAnnouncementsProvider.test.tsx index fe31abdbaee..727f7832bb4 100644 --- a/packages/sanity/src/core/studio/studioAnnouncements/__tests__/StudioAnnouncementsProvider.test.tsx +++ b/packages/sanity/src/core/studio/studioAnnouncements/__tests__/StudioAnnouncementsProvider.test.tsx @@ -2,7 +2,7 @@ import {fireEvent, render, renderHook, waitFor} from '@testing-library/react' import {type ReactNode} from 'react' import {of} from 'rxjs' -import {defineConfig} from 'sanity' +import {type Config, defineConfig} from 'sanity' import {beforeAll, beforeEach, describe, expect, test, vi} from 'vitest' import {createTestProvider} from '../../../../../test/testUtils/TestProvider' @@ -36,10 +36,11 @@ vi.mock('@sanity/client', () => ({ vi.mock('../../../hooks/useClient') const useClientMock = useClient as ReturnType +const mockObservableRequest = vi.fn((announcements) => of(announcements)) const mockClient = (announcements: StudioAnnouncementDocument[]) => { useClientMock.mockReturnValue({ observable: { - request: () => of(announcements), + request: () => mockObservableRequest(announcements), }, }) } @@ -55,9 +56,12 @@ const config = defineConfig({ projectId: 'test', dataset: 'test', }) -async function createAnnouncementWrapper() { +async function createAnnouncementWrapper(configOverride: Partial = {}) { const wrapper = await createTestProvider({ - config, + config: { + ...config, + ...configOverride, + }, resources: [], }) @@ -114,7 +118,8 @@ describe('StudioAnnouncementsProvider', () => { const {result} = renderHook(() => useStudioAnnouncements(), { wrapper, }) - + expect(seenAnnouncementsMock).toBeCalled() + expect(mockObservableRequest).toBeCalled() expect(result.current.unseenAnnouncements).toEqual([]) expect(result.current.studioAnnouncements).toEqual(mockAnnouncements) }) @@ -641,3 +646,24 @@ describe('StudioAnnouncementsProvider', () => { }) }) }) + +describe('StudioAnnouncementsProvider-Disabled', () => { + let wrapper = ({children}: {children: ReactNode}) => children + beforeAll(async () => { + // Reset all mocks + vi.clearAllMocks() + wrapper = await createAnnouncementWrapper({ + announcements: {enabled: false}, + }) + }) + test('if the feature is disabled, the client should not be called', () => { + const {result} = renderHook(() => useStudioAnnouncements(), { + wrapper, + }) + + expect(result.current.unseenAnnouncements).toEqual([]) + expect(result.current.studioAnnouncements).toEqual([]) + expect(seenAnnouncementsMock).not.toBeCalled() + expect(mockObservableRequest).not.toBeCalled() + }) +}) diff --git a/packages/sanity/src/core/studio/studioAnnouncements/useStudioAnnouncements.tsx b/packages/sanity/src/core/studio/studioAnnouncements/useStudioAnnouncements.tsx index 2ff5fba557f..b61b0a251be 100644 --- a/packages/sanity/src/core/studio/studioAnnouncements/useStudioAnnouncements.tsx +++ b/packages/sanity/src/core/studio/studioAnnouncements/useStudioAnnouncements.tsx @@ -5,8 +5,13 @@ import {type StudioAnnouncementsContextValue} from './types' export function useStudioAnnouncements(): StudioAnnouncementsContextValue { const context = useContext(StudioAnnouncementContext) + if (!context) { - throw new Error('useStudioAnnouncements: missing context value') + return { + studioAnnouncements: [], + unseenAnnouncements: [], + onDialogOpen: () => {}, + } } return context