diff --git a/packages/sanity/src/core/bundles/util/util.ts b/packages/sanity/src/core/bundles/util/util.ts index dd06aa276dc..3a1804148be 100644 --- a/packages/sanity/src/core/bundles/util/util.ts +++ b/packages/sanity/src/core/bundles/util/util.ts @@ -1,5 +1,5 @@ import {type BundleDocument} from '../../store/bundles/types' -import {getVersionFromId, isVersionId} from '../../util' +import {getVersionFromId, isVersionId, resolveBundlePerspective} from '../../util' /** * @beta @@ -22,11 +22,13 @@ export function getDocumentIsInPerspective( if (!perspective) return !isVersionId(documentId) - if (!perspective.startsWith('bundle.')) return false + const bundlePerspective = resolveBundlePerspective(perspective) + + if (typeof bundlePerspective === 'undefined') return false // perspective is `bundle.${bundleId}` if (bundleId === 'Published') return false - return bundleId === perspective.replace('bundle.', '') + return bundleId === bundlePerspective } export function versionDocumentExists( diff --git a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx index 049d7abb152..b7d69ad1f53 100644 --- a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx +++ b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx @@ -6,7 +6,7 @@ import {CommentsContext} from 'sanity/_singletons' import {useEditState, useSchema, useUserListWithPermissions} from '../../../hooks' import {useCurrentUser} from '../../../store' import {useAddonDataset, useWorkspace} from '../../../studio' -import {getPublishedId} from '../../../util' +import {getPublishedId, resolveBundlePerspective} from '../../../util' import { type CommentOperationsHookOptions, useCommentOperations, @@ -85,15 +85,11 @@ export const CommentsProvider = memo(function CommentsProvider(props: CommentsPr const [status, setStatus] = useState('open') const {client, createAddonDataset, isCreatingDataset} = useAddonDataset() - const bundlePerspective = perspective?.startsWith('bundle.') - ? perspective.split('bundle.').at(1) - : undefined - const editState = useEditState( getPublishedId(versionOrPublishedId), documentType, 'default', - bundlePerspective, + resolveBundlePerspective(perspective), ) const schemaType = useSchema().get(documentType) const currentUser = useCurrentUser() diff --git a/packages/sanity/src/core/preview/utils/getPreviewValueWithFallback.tsx b/packages/sanity/src/core/preview/utils/getPreviewValueWithFallback.tsx index ea1a9109c78..ba4ca303e38 100644 --- a/packages/sanity/src/core/preview/utils/getPreviewValueWithFallback.tsx +++ b/packages/sanity/src/core/preview/utils/getPreviewValueWithFallback.tsx @@ -2,6 +2,8 @@ import {WarningOutlineIcon} from '@sanity/icons' import {type PreviewValue, type SanityDocument} from '@sanity/types' import {assignWith} from 'lodash' +import {resolveBundlePerspective} from '../../util' + const getMissingDocumentFallback = (item: SanityDocument) => ({ title: {item.title ? String(item.title) : 'Missing document'}, subtitle: {item.title ? `Missing document ID: ${item._id}` : `Document ID: ${item._id}`}, @@ -30,7 +32,7 @@ export const getPreviewValueWithFallback = ({ let snapshot: Partial | PreviewValue | null | undefined switch (true) { - case perspective?.startsWith('bundle.'): + case typeof resolveBundlePerspective(perspective) !== 'undefined': snapshot = version || draft || published break case perspective === 'published': diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx index 1ca03684766..e8cb95844ed 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx @@ -3,7 +3,7 @@ import {type SchemaType} from '@sanity/types' import {Badge, Box, Flex} from '@sanity/ui' import {useMemo} from 'react' import {useObservable} from 'react-rx' -import {getPublishedId} from 'sanity' +import {getPublishedId, resolveBundlePerspective} from 'sanity' import {styled} from 'styled-components' import {type GeneralPreviewLayoutKey} from '../../../../../../../components' @@ -59,7 +59,7 @@ export function SearchResultItemPreview({ schemaType, getPublishedId(documentId), '', - perspective?.startsWith('bundle.') ? perspective.split('bundle.').at(1) : undefined, + resolveBundlePerspective(perspective), ), [documentId, documentPreviewStore, perspective, schemaType], ) diff --git a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx index 7055f78c01c..4905bb360fe 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx @@ -8,6 +8,7 @@ import {type CommandListHandle} from '../../../../../../components' import {useSchema} from '../../../../../../hooks' import {type SearchTerms} from '../../../../../../search' import {useCurrentUser} from '../../../../../../store' +import {resolvePerspectiveOptions} from '../../../../../../util/resolvePerspective' import {useSource} from '../../../../../source' import {SEARCH_LIMIT} from '../../constants' import {type RecentSearch} from '../../datastores/recentSearches' @@ -142,10 +143,9 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { skipSortByScore: ordering.ignoreScore, ...(ordering.sort ? {sort: [ordering.sort]} : {}), cursor: cursor || undefined, - perspective: omitBundlePerspective(perspective), - bundlePerspective: perspective?.startsWith('bundle.') - ? [perspective.split('bundle.').at(1), DRAFTS_FOLDER].join(',') - : undefined, + ...resolvePerspectiveOptions(perspective, (perspectives, isSystemPerspective) => + isSystemPerspective ? perspectives : perspectives.concat(DRAFTS_FOLDER), + ), }, terms: { ...terms, @@ -204,11 +204,3 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { return {children} } - -function omitBundlePerspective(perspective: string | undefined): string | undefined { - if (perspective?.startsWith('bundle.')) { - return undefined - } - - return perspective -} diff --git a/packages/sanity/src/core/util/__tests__/resolvePerspectives.test.ts b/packages/sanity/src/core/util/__tests__/resolvePerspectives.test.ts new file mode 100644 index 00000000000..812c2bf6df9 --- /dev/null +++ b/packages/sanity/src/core/util/__tests__/resolvePerspectives.test.ts @@ -0,0 +1,74 @@ +import {describe, expect, it} from '@jest/globals' + +import {resolveBundlePerspective, resolvePerspectiveOptions} from '../resolvePerspective' + +describe('resolveBundlePerspective', () => { + it('returns the perspective with the `bundle.` prefix removed', () => { + expect(resolveBundlePerspective('bundle.x')).toBe('x') + }) + + it('returns `undefined` if the provided perspective has no `bundle.` prefix', () => { + expect(resolveBundlePerspective('x')).toBeUndefined() + }) + + it('returns `undefined` if no perspective is provided', () => { + expect(resolveBundlePerspective()).toBeUndefined() + }) +}) + +describe('resolvePerspectiveOptions', () => { + it('includes the `bundlePerspective` property if a bundle is provided', () => { + expect(resolvePerspectiveOptions('bundle.x')).toHaveProperty('bundlePerspective') + expect(resolvePerspectiveOptions('bundle.x')).not.toHaveProperty('perspective') + }) + + it('includes the `perspective` property if a system perspective is provided', () => { + expect(resolvePerspectiveOptions('x')).toHaveProperty('perspective') + expect(resolvePerspectiveOptions('x')).not.toHaveProperty('bundlePerspective') + }) + + it(`removes the bundle prefix if it exists`, () => { + expect(resolvePerspectiveOptions('bundle.x').bundlePerspective).toEqual('x') + expect(resolvePerspectiveOptions('x').perspective).toEqual('x') + }) + + it('allows the extracted perspectives to be transformed', () => { + expect(resolvePerspectiveOptions('x', () => ['y'])).toEqual({ + perspective: 'y', + }) + }) + + it('passes the perspective to the `transformPerspectives` function', () => { + expect.assertions(2) + + resolvePerspectiveOptions('x', (perspectives) => { + expect(perspectives).toEqual(['x']) + return perspectives + }) + + resolvePerspectiveOptions('bundle.x', (perspectives) => { + expect(perspectives).toEqual(['x']) + return perspectives + }) + }) + + it('passes the perspective type to the `transformPerspectives` function', () => { + expect.assertions(2) + + resolvePerspectiveOptions('x', (perspectives, isSystemPerspective) => { + expect(isSystemPerspective).toBe(true) + return perspectives + }) + + resolvePerspectiveOptions('bundle.x', (perspectives, isSystemPerspective) => { + expect(isSystemPerspective).toBe(false) + return perspectives + }) + }) + + it('produces a correctly formatted list of perspectives', () => { + expect(resolvePerspectiveOptions('x', (perspectives) => perspectives.concat('y'))).toEqual({ + perspective: 'x,y', + }) + }) +}) diff --git a/packages/sanity/src/core/util/index.ts b/packages/sanity/src/core/util/index.ts index 15ead1cc190..a34f563d5d6 100644 --- a/packages/sanity/src/core/util/index.ts +++ b/packages/sanity/src/core/util/index.ts @@ -9,6 +9,7 @@ export * from './isString' export * from './isTruthy' export * from './PartialExcept' export * from './resizeObserver' +export * from './resolvePerspective' export * from './schemaUtils' export * from './searchUtils' export * from './supportsTouch' diff --git a/packages/sanity/src/core/util/resolvePerspective.ts b/packages/sanity/src/core/util/resolvePerspective.ts new file mode 100644 index 00000000000..1801d09f900 --- /dev/null +++ b/packages/sanity/src/core/util/resolvePerspective.ts @@ -0,0 +1,42 @@ +/** + * If the provided perspective has the `bundle.` prefix, returns it with this + * prefix removed. + * + * @internal + */ +export function resolveBundlePerspective(perspective?: string): string | undefined { + return perspective?.split(/^bundle./).at(1) +} + +/** + * Given a system perspective, or a bundle name prefixed with `bundle.`, returns + * an object with either `perspective` or `bundlePerspective` properties that + * may be submitted directly to Content Lake APIs. + * + * @internal + */ +export function resolvePerspectiveOptions( + perspective: string | undefined, + transformPerspectives: (perspectives: string[], isSystemPerspective: boolean) => string[] = ( + perspectives, + ) => perspectives, +): + | {perspective: string; bundlePerspective?: never} + | {perspective?: never; bundlePerspective: string} + | Record { + if (typeof perspective === 'undefined') { + return {} + } + + const bundlePerspective = resolveBundlePerspective(perspective) + + if (typeof bundlePerspective === 'string') { + return { + bundlePerspective: transformPerspectives([bundlePerspective], false).join(','), + } + } + + return { + perspective: transformPerspectives([perspective], true).join(','), + } +} diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx index ab7795b2a3c..0a88a37db78 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx @@ -13,6 +13,7 @@ import { getPreviewStateObservable, getPreviewValueWithFallback, isRecord, + resolveBundlePerspective, SanityDefaultPreview, } from 'sanity' import {styled} from 'styled-components' @@ -56,7 +57,7 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { schemaType, value._id, title, - perspective?.startsWith('bundle.') ? perspective.split('bundle.').at(1) : undefined, + resolveBundlePerspective(perspective), ), [props.documentPreviewStore, schemaType, title, value._id, perspective], ) diff --git a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx index ac93b3dc194..2bb83344f82 100644 --- a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx +++ b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx @@ -1,6 +1,7 @@ import {type ObjectSchemaType} from '@sanity/types' import {useEffect} from 'react' import { + resolveBundlePerspective, unstable_useValuePreview as useValuePreview, useEditState, useSchema, @@ -26,11 +27,12 @@ const DocumentTitle = (props: {documentId: string; documentType: string}) => { const paneRouter = usePaneRouter() const perspective = paneRouter.perspective ?? router.stickyParams.perspective - const bundlePerspective = perspective?.startsWith('bundle.') - ? perspective.split('bundle.').at(1) - : undefined - - const editState = useEditState(documentId, documentType, 'default', bundlePerspective) + const editState = useEditState( + documentId, + documentType, + 'default', + resolveBundlePerspective(perspective), + ) const schema = useSchema() const {t} = useTranslation(structureLocaleNamespace) const isNewDocument = !editState?.published && !editState?.draft diff --git a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx index c00dde3adfb..9eaeb06a04b 100644 --- a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx +++ b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx @@ -23,6 +23,7 @@ import { isVersionId, type OnPathFocusPayload, type PatchEvent, + resolveBundlePerspective, setAtPath, type StateTree, toMutationPatches, @@ -99,9 +100,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { const params = useUnique(paneRouter.params) || EMPTY_PARAMS const {perspective} = paneRouter - const bundlePerspective = perspective?.startsWith('bundle.') - ? perspective.split('bundle.').at(1) - : undefined + const bundlePerspective = resolveBundlePerspective(perspective) /* Version and the global perspective should match. * If user clicks on add document, and then switches to another version, he should click again on create document. diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx index 07dbd1b2afb..09d30b85963 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx @@ -9,6 +9,7 @@ import {debounceTime, map} from 'rxjs/operators' import { type DocumentAvailability, getPublishedId, + resolveBundlePerspective, useDocumentPreviewStore, useTranslation, } from 'sanity' @@ -44,9 +45,7 @@ export const ReferenceChangedBanner = memo(() => { }, [params?.parentRefPath]) const {t} = useTranslation(structureLocaleNamespace) - const bundlePerspective = perspective?.startsWith('bundle.') - ? perspective.split('bundle.').at(1) - : undefined + const bundlePerspective = resolveBundlePerspective(perspective) /** * Loads information regarding the reference field of the parent pane. This diff --git a/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts b/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts index 12f25645662..34e5edc51af 100644 --- a/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts +++ b/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts @@ -7,6 +7,7 @@ import { type FormNodeValidation, isValidationError, isValidationWarning, + resolveBundlePerspective, useTranslation, useValidationStatus, } from 'sanity' @@ -20,14 +21,10 @@ function useMenuItem(props: DocumentInspectorUseMenuItemProps): DocumentInspecto const {t} = useTranslation('validation') const paneRouter = usePaneRouter() - const bundlePerspective = paneRouter.perspective?.startsWith('bundle.') - ? paneRouter.perspective.split('bundle.').at(1) - : undefined - const {validation: validationMarkers} = useValidationStatus( documentId, documentType, - bundlePerspective, + resolveBundlePerspective(paneRouter.perspective), ) const validation: FormNodeValidation[] = useMemo( diff --git a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts index c6a14d32b5d..61a9791703d 100644 --- a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts +++ b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts @@ -19,8 +19,10 @@ import { createSearch, DRAFTS_FOLDER, getSearchableTypes, + resolvePerspectiveOptions, type SanityDocumentLike, type Schema, + type SearchOptions, } from 'sanity' import {getExtendedProjection} from '../../structureBuilder/util/getExtendedProjection' @@ -128,16 +130,15 @@ export function listenSearchQuery(options: ListenQueryOptions): Observable + isSystemPerspective ? perspectives : perspectives.concat(DRAFTS_FOLDER), + ), } return search(searchTerms, searchOptions).pipe( @@ -160,11 +161,3 @@ export function listenSearchQuery(options: ListenQueryOptions): Observable