From da3f744f691d28a31bcdd0b3d56a8e1f782667af Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 15 Aug 2024 01:31:04 +0100 Subject: [PATCH] feat(sanity): adopt `bundlePerspective` for listing documents --- .../sanity/src/core/search/common/types.ts | 2 + .../search/text-search/createTextSearch.ts | 4 ++ .../search/weighted/createSearchQuery.test.ts | 18 ++++---- .../core/search/weighted/createSearchQuery.ts | 9 +++- .../searchResults/item/SearchResultItem.tsx | 5 ++- .../item/SearchResultItemPreview.tsx | 24 ++++++----- .../search/contexts/search/SearchProvider.tsx | 17 +++++++- packages/sanity/src/core/util/draftUtils.ts | 42 ++----------------- .../components/paneItem/PaneItemPreview.tsx | 10 ++--- .../structure/panes/documentList/helpers.ts | 9 ++-- .../panes/documentList/listenSearchQuery.ts | 11 ++++- .../panes/documentList/useDocumentList.ts | 9 +--- 12 files changed, 77 insertions(+), 83 deletions(-) diff --git a/packages/sanity/src/core/search/common/types.ts b/packages/sanity/src/core/search/common/types.ts index 3b4784f9b5fd..967fe5ba1385 100644 --- a/packages/sanity/src/core/search/common/types.ts +++ b/packages/sanity/src/core/search/common/types.ts @@ -107,6 +107,7 @@ export type SearchOptions = { cursor?: string limit?: number perspective?: string + bundlePerspective?: string isCrossDataset?: boolean queryType?: 'prefixLast' | 'prefixNone' } @@ -190,6 +191,7 @@ export type TextSearchParams = { */ order?: TextSearchOrder[] perspective?: string + bundlePerspective?: string } export type TextSearchResponse> = { diff --git a/packages/sanity/src/core/search/text-search/createTextSearch.ts b/packages/sanity/src/core/search/text-search/createTextSearch.ts index 8f70a904bf65..1093e4fd7347 100644 --- a/packages/sanity/src/core/search/text-search/createTextSearch.ts +++ b/packages/sanity/src/core/search/text-search/createTextSearch.ts @@ -142,10 +142,14 @@ export const createTextSearch: SearchStrategyFactory = ( searchOptions.includeDrafts === false && "!(_id in path('drafts.**'))", factoryOptions.filter ? `(${factoryOptions.filter})` : false, searchTerms.filter ? `(${searchTerms.filter})` : false, + // Versions are collated server-side using the `bundlePerspective` option. Therefore, they + // must not be fetched individually. + '!(_id in path("versions.**"))', ].filter((baseFilter): baseFilter is string => Boolean(baseFilter)) const textSearchParams: TextSearchParams = { perspective: searchOptions.perspective, + bundlePerspective: searchOptions.bundlePerspective, query: { string: getQueryString(searchTerms.query, searchOptions), }, diff --git a/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts b/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts index 1ed2e951bfd1..6b12f3c2be96 100644 --- a/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts +++ b/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts @@ -46,7 +46,7 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && !(_id in path("versions.**"))]' + '| order(_id asc)' + '[0...$__limit]' + '{_type, _id, _version, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', @@ -106,7 +106,7 @@ describe('createSearchQuery', () => { }) expect(query).toContain( - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0 || object.field match $t0)]', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0 || object.field match $t0) && !(_id in path("versions.**"))]', ) }) @@ -117,7 +117,7 @@ describe('createSearchQuery', () => { }) expect(query).toContain( - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (_id match $t1 || _type match $t1 || title match $t1)]', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (_id match $t1 || _type match $t1 || title match $t1) && !(_id in path("versions.**"))]', ) expect(params.t0).toEqual('term0*') expect(params.t1).toEqual('term1*') @@ -147,7 +147,7 @@ describe('createSearchQuery', () => { const result = [ `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]{_type, _id, _version, object{field}}', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && !(_id in path("versions.**"))]{_type, _id, _version, object{field}}', '|order(_id asc)[0...$__limit]', '{_type, _id, _version, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ].join('') @@ -193,7 +193,7 @@ describe('createSearchQuery', () => { ) expect(query).toContain( - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (randomCondition == $customParam)]', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (randomCondition == $customParam) && !(_id in path("versions.**"))]', ) expect(params.customParam).toEqual('custom') }) @@ -241,7 +241,7 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && !(_id in path("versions.**"))]' + '| order(exampleField desc)' + '[0...$__limit]' + '{_type, _id, _version, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', @@ -275,7 +275,7 @@ describe('createSearchQuery', () => { const result = [ `// findability-mvi:${FINDABILITY_MVI}\n`, - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]| ', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && !(_id in path("versions.**"))]| ', 'order(exampleField desc,anotherExampleField asc,lower(mapWithField) asc)', '[0...$__limit]{_type, _id, _version, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ].join('') @@ -291,7 +291,7 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && !(_id in path("versions.**"))]' + '| order(_id asc)' + '[0...$__limit]' + '{_type, _id, _version, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', @@ -403,7 +403,7 @@ describe('createSearchQuery', () => { * This is an improvement over before, where an illegal term was used (number-as-string, ala ["0"]), * which lead to no hits at all. */ `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (_id match $t0 || _type match $t0 || cover[].cards[].title match $t0) && (_id match $t1 || _type match $t1 || cover[].cards[].title match $t1)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || cover[].cards[].title match $t0) && (_id match $t1 || _type match $t1 || cover[].cards[].title match $t1) && !(_id in path("versions.**"))]' + '| order(_id asc)' + '[0...$__limit]' + // at this point we could refilter using cover[0].cards[0].title. diff --git a/packages/sanity/src/core/search/weighted/createSearchQuery.ts b/packages/sanity/src/core/search/weighted/createSearchQuery.ts index 1b83a0967b86..500ed7ef806e 100644 --- a/packages/sanity/src/core/search/weighted/createSearchQuery.ts +++ b/packages/sanity/src/core/search/weighted/createSearchQuery.ts @@ -135,6 +135,9 @@ export function createSearchQuery( ...createConstraints(terms, specs), filter ? `(${filter})` : '', searchTerms.filter ? `(${searchTerms.filter})` : '', + // Versions are collated server-side using the `bundlePerspective` option. Therefore, they must + // not be fetched individually. + '!(_id in path("versions.**"))', ].filter(Boolean) const selections = specs.map((spec) => { @@ -186,7 +189,11 @@ export function createSearchQuery( __limit: limit, ...(params || {}), }, - options: {tag, perspective: searchOpts.perspective}, + options: { + tag, + perspective: searchOpts.perspective, + bundlePerspective: searchOpts.bundlePerspective, + }, searchSpec: specs, terms, } diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItem.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItem.tsx index 0ffbeac0b8e3..09ff7a746092 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItem.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItem.tsx @@ -1,7 +1,7 @@ import {type SanityDocumentLike} from '@sanity/types' import {Box, type ResponsiveMarginProps, type ResponsivePaddingProps} from '@sanity/ui' import {type MouseEvent, useCallback, useMemo} from 'react' -import {useIntentLink} from 'sanity/router' +import {useIntentLink, useRouter} from 'sanity/router' import {type GeneralPreviewLayoutKey, PreviewCard} from '../../../../../../../components' import {useSchema} from '../../../../../../../hooks' @@ -31,7 +31,7 @@ export function SearchResultItem({ const schema = useSchema() const type = schema.get(documentType) const documentPresence = useDocumentPresence(documentId) - + const perspective = useRouter().stickyParams.perspective const params = useMemo(() => ({id: documentId, type: type?.name}), [documentId, type?.name]) const {onClick: onIntentClick, href} = useIntentLink({ intent: 'edit', @@ -65,6 +65,7 @@ export function SearchResultItem({ 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 a267d2658af1..1ca036847662 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, getVersionFromId, isVersionId} from 'sanity' +import {getPublishedId} from 'sanity' import {styled} from 'styled-components' import {type GeneralPreviewLayoutKey} from '../../../../../../../components' @@ -19,6 +19,7 @@ import {type DocumentPresence, useDocumentPreviewStore} from '../../../../../../ interface SearchResultItemPreviewProps { documentId: string + perspective?: string layout?: GeneralPreviewLayoutKey presence?: DocumentPresence[] schemaType: SchemaType @@ -29,7 +30,8 @@ interface SearchResultItemPreviewProps { * Temporary workaround: force all nested boxes on iOS to use `background-attachment: scroll` * to allow components to render correctly within virtual lists. */ -const SearchResultItemPreviewBox = styled(Box)` +const SearchResultItemPreviewBox = styled(Box)<{$isInPerspective: boolean}>` + opacity: ${(props) => (props.$isInPerspective ? 1 : 0.5)}; @supports (-webkit-overflow-scrolling: touch) { * [data-ui='Box'] { background-attachment: scroll; @@ -43,6 +45,7 @@ const SearchResultItemPreviewBox = styled(Box)` export function SearchResultItemPreview({ documentId, layout, + perspective, presence, schemaType, showBadge = true, @@ -56,15 +59,16 @@ export function SearchResultItemPreview({ schemaType, getPublishedId(documentId), '', - getVersionFromId(documentId), + perspective?.startsWith('bundle.') ? perspective.split('bundle.').at(1) : undefined, ), - [documentId, documentPreviewStore, schemaType], + [documentId, documentPreviewStore, perspective, schemaType], ) const {draft, published, isLoading, version} = useObservable(observable, { draft: null, isLoading: true, published: null, + version: null, }) const sanityDocument = useMemo(() => { @@ -80,24 +84,22 @@ export function SearchResultItemPreview({ {presence && presence.length > 0 && } {showBadge && {schemaType.title}} - + ) - }, [draft, isLoading, presence, published, schemaType.title, showBadge]) + }, [draft, isLoading, presence, published, schemaType.title, showBadge, version]) - const tooltip = + const tooltip = return ( - + void) | null>(null) const [searchCommandList, setSearchCommandList] = useState(null) - + const perspective = useRouter().stickyParams.perspective const schema = useSchema() const currentUser = useCurrentUser() const { @@ -140,6 +142,10 @@ 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, }, terms: { ...terms, @@ -165,6 +171,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { searchState.terms, terms, cursor, + perspective, ]) /** @@ -197,3 +204,11 @@ 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/draftUtils.ts b/packages/sanity/src/core/util/draftUtils.ts index 8bf591653e7b..431012cdbbb5 100644 --- a/packages/sanity/src/core/util/draftUtils.ts +++ b/packages/sanity/src/core/util/draftUtils.ts @@ -177,57 +177,23 @@ export interface CollatedHit { type: string draft?: T published?: T - version?: T -} - -interface CollateOptions { - bundlePerspective?: string } /** @internal */ -export function collate< - T extends { - _id: string - _type: string - }, ->(documents: T[], {bundlePerspective}: CollateOptions = {}): CollatedHit[] { +export function collate(documents: T[]): CollatedHit[] { const byId = documents.reduce((res, doc) => { const publishedId = getPublishedId(doc._id) - const isVersion = isVersionId(doc._id) - const bundle = getVersionFromId(doc._id) - let entry = res.get(publishedId) if (!entry) { - entry = { - id: publishedId, - type: doc._type, - published: undefined, - draft: undefined, - version: undefined, - } + entry = {id: publishedId, type: doc._type, published: undefined, draft: undefined} res.set(publishedId, entry) } - if (bundlePerspective && bundle === bundlePerspective) { - entry.version = doc - } - - if (!isVersion) { - entry[publishedId === doc._id ? 'published' : 'draft'] = doc - } - + entry[publishedId === doc._id ? 'published' : 'draft'] = doc return res }, new Map()) - return ( - Array.from(byId.values()) - // Remove entries that have no data, because all the following conditions are true: - // - // 1. They have no published version. - // 2. They have no draft version. - // 3. They have a version, but not the one that is currently checked out. - .filter((entry) => entry.published ?? entry.version ?? entry.draft) - ) + return Array.from(byId.values()) } /** @internal */ diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx index e47a13ae481c..ab7795b2a3cb 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx @@ -10,7 +10,6 @@ import { DocumentStatus, DocumentStatusIndicator, type GeneralPreviewLayoutKey, - getDocumentIsInPerspective, getPreviewStateObservable, getPreviewValueWithFallback, isRecord, @@ -66,13 +65,10 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { draft: null, isLoading: true, published: null, + version: null, + perspective, }) - const isInPerspective = useMemo( - () => getDocumentIsInPerspective(value._id, perspective), - [perspective, value._id], - ) - const status = isLoading ? null : ( @@ -85,7 +81,7 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { const tooltip = return ( - + { - const doc = entry.version || entry.draft || entry.published +export function removePublishedWithDrafts(documents: SanityDocumentLike[]): DocumentListPaneItem[] { + return collate(documents).map((entry) => { + const doc = entry.draft || entry.published const isVersion = doc?.id && isVersionId(doc._id) const hasDraft = Boolean(entry.draft) diff --git a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts index 19c321bf663d..c6a14d32b5d0 100644 --- a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts +++ b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts @@ -15,7 +15,13 @@ import { timer, } from 'rxjs' import {exhaustMapWithTrailing} from 'rxjs-exhaustmap-with-trailing' -import {createSearch, getSearchableTypes, type SanityDocumentLike, type Schema} from 'sanity' +import { + createSearch, + DRAFTS_FOLDER, + getSearchableTypes, + type SanityDocumentLike, + type Schema, +} from 'sanity' import {getExtendedProjection} from '../../structureBuilder/util/getExtendedProjection' // FIXME @@ -127,6 +133,9 @@ export function listenSearchQuery(options: ListenQueryOptions): Observable - documents - ? removePublishedWithDrafts(documents, { - bundlePerspective: (perspective ?? '').split('bundle.').at(1), - }) - : EMPTY_ARRAY, - [documents, perspective], + () => (documents ? removePublishedWithDrafts(documents) : EMPTY_ARRAY), + [documents], ) // A state variable to keep track of whether we are currently lazy loading the list.