From 16a2f631d04b1fabb543e68c86ec0fba31540f0b Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 24 Oct 2024 14:22:39 +0100 Subject: [PATCH] feat(sanity): add release layering foundations --- .../utils/getPreviewStateObservable.ts | 73 ++++++++++++++++--- .../src/core/store/_legacy/datastores.ts | 10 ++- .../core/store/bundles/createBundlesStore.ts | 5 +- .../sanity/src/core/store/bundles/reducer.ts | 57 +++++++++++++++ .../src/core/store/bundles/useBundles.ts | 9 +++ .../item/SearchResultItemPreview.tsx | 51 ++++++++----- .../search/contexts/search/SearchProvider.tsx | 12 +-- .../src/core/util/resolvePerspective.ts | 31 +++----- .../components/paneItem/PaneItem.tsx | 2 +- .../components/paneItem/PaneItemPreview.tsx | 62 +++++++++------- .../panes/documentList/DocumentListPane.tsx | 33 +++++---- .../panes/documentList/listenSearchQuery.ts | 7 +- .../panes/documentList/useDocumentList.ts | 2 +- 13 files changed, 247 insertions(+), 107 deletions(-) diff --git a/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts b/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts index 2351a65262b..d773b3c8108 100644 --- a/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts +++ b/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts @@ -1,16 +1,26 @@ import {type PreviewValue, type SanityDocument, type SchemaType} from '@sanity/types' +import {omit} from 'lodash' import {type ReactNode} from 'react' -import {combineLatest, type Observable, of} from 'rxjs' -import {map, startWith} from 'rxjs/operators' +import {combineLatest, from, type Observable, of} from 'rxjs' +import {map, mergeMap, scan, startWith} from 'rxjs/operators' +import {type PreparedSnapshot} from 'sanity' import {getDraftId, getPublishedId, getVersionId} from '../../util/draftUtils' import {type DocumentPreviewStore} from '../documentPreviewStore' +/** + * @internal + */ +export type VersionsRecord = Record + +type VersionTuple = [bundleId: string, snapshot: PreparedSnapshot] + export interface PreviewState { isLoading?: boolean draft?: PreviewValue | Partial | null published?: PreviewValue | Partial | null version?: PreviewValue | Partial | null + versions: VersionsRecord } const isLiveEditEnabled = (schemaType: SchemaType) => schemaType.liveEdit === true @@ -25,31 +35,70 @@ export function getPreviewStateObservable( schemaType: SchemaType, documentId: string, title: ReactNode, - perspective?: string, + perspective: { + bundleIds: string[] + bundleStack: string[] + } = { + bundleIds: [], + bundleStack: [], + }, ): Observable { const draft$ = isLiveEditEnabled(schemaType) ? of({snapshot: null}) : documentPreviewStore.observeForPreview({_id: getDraftId(documentId)}, schemaType) - const version$ = perspective - ? documentPreviewStore.observeForPreview( - {_id: getVersionId(documentId, perspective)}, - schemaType, - ) - : of({snapshot: null}) + const versions$ = from(perspective.bundleIds).pipe( + mergeMap>((bundleId) => + documentPreviewStore + .observeForPreview({_id: getVersionId(documentId, bundleId)}, schemaType) + .pipe(map((storeValue) => [bundleId, storeValue])), + ), + scan((byBundleId, [bundleId, value]) => { + if (value.snapshot === null) { + return omit({...byBundleId}, [bundleId]) + } + + return { + ...byBundleId, + [bundleId]: value, + } + }, {}), + startWith({}), + ) + + // Iterate the release stack in descending precedence, returning the highest precedence existing + // version document. + const version$ = versions$.pipe( + map((versions) => { + for (const bundleId of perspective.bundleStack) { + if (bundleId in versions) { + return versions[bundleId] + } + } + return {snapshot: null} + }), + startWith({snapshot: null}), + ) const published$ = documentPreviewStore.observeForPreview( {_id: getPublishedId(documentId)}, schemaType, ) - return combineLatest([draft$, published$, version$]).pipe( - map(([draft, published, version]) => ({ + return combineLatest([draft$, published$, version$, versions$]).pipe( + map(([draft, published, version, versions]) => ({ draft: draft.snapshot ? {title, ...(draft.snapshot || {})} : null, isLoading: false, published: published.snapshot ? {title, ...(published.snapshot || {})} : null, version: version.snapshot ? {title, ...(version.snapshot || {})} : null, + versions, })), - startWith({draft: null, isLoading: true, published: null, version: null}), + startWith({ + draft: null, + isLoading: true, + published: null, + version: null, + versions: {}, + }), ) } diff --git a/packages/sanity/src/core/store/_legacy/datastores.ts b/packages/sanity/src/core/store/_legacy/datastores.ts index b4ed499ecee..8b6c8770848 100644 --- a/packages/sanity/src/core/store/_legacy/datastores.ts +++ b/packages/sanity/src/core/store/_legacy/datastores.ts @@ -4,6 +4,7 @@ import {useTelemetry} from '@sanity/telemetry/react' import {useCallback, useMemo} from 'react' import {of} from 'rxjs' +import {useRouter} from '../../../router' import {useClient, useSchema, useTemplates} from '../../hooks' import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../preview' import {useAddonDataset, useSource, useWorkspace} from '../../studio' @@ -299,11 +300,13 @@ export function useBundlesStore(): BundlesStore { const currentUser = useCurrentUser() const addonDataset = useAddonDataset() const studioClient = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) + const router = useRouter() + // TODO: Include hidden layers state. return useMemo(() => { const bundlesStore = resourceCache.get({ - dependencies: [workspace, addonDataset, currentUser], + dependencies: [workspace, addonDataset, currentUser, router.perspectiveState], namespace: 'BundlesStore', }) || createBundlesStore({ @@ -311,14 +314,15 @@ export function useBundlesStore(): BundlesStore { addonClientReady: addonDataset.ready, studioClient, currentUser, + perspective: router.perspectiveState.perspective, }) resourceCache.set({ - dependencies: [workspace, addonDataset, currentUser], + dependencies: [workspace, addonDataset, currentUser, router.perspectiveState], namespace: 'BundlesStore', value: bundlesStore, }) return bundlesStore - }, [resourceCache, workspace, addonDataset, studioClient, currentUser]) + }, [resourceCache, workspace, addonDataset, currentUser, router.perspectiveState, studioClient]) } diff --git a/packages/sanity/src/core/store/bundles/createBundlesStore.ts b/packages/sanity/src/core/store/bundles/createBundlesStore.ts index 5c6d22af7f2..fd1df27ba09 100644 --- a/packages/sanity/src/core/store/bundles/createBundlesStore.ts +++ b/packages/sanity/src/core/store/bundles/createBundlesStore.ts @@ -55,6 +55,7 @@ const INITIAL_STATE: bundlesReducerState = { bundles: new Map(), deletedBundles: {}, state: 'initialising', + releaseStack: [], } const NOOP_BUNDLE_STORE: BundlesStore = { @@ -69,6 +70,7 @@ const LOADED_BUNDLE_STORE: BundlesStore = { bundles: new Map(), deletedBundles: {}, state: 'loaded' as const, + releaseStack: [], }), ), getMetadataStateForSlugs$: () => of({data: null, error: null, loading: false}), @@ -88,6 +90,7 @@ export function createBundlesStore(context: { studioClient: SanityClient | null addonClientReady: boolean currentUser: User | null + perspective?: string }): BundlesStore { const {addonClient, studioClient, addonClientReady, currentUser} = context @@ -251,7 +254,7 @@ export function createBundlesStore(context: { const state$ = merge(listFetch$, listener$, dispatch$).pipe( filter((action): action is bundlesReducerAction => typeof action !== 'undefined'), - scan((state, action) => bundlesReducer(state, action), INITIAL_STATE), + scan((state, action) => bundlesReducer(state, action, context.perspective), INITIAL_STATE), startWith(INITIAL_STATE), shareReplay(1), ) diff --git a/packages/sanity/src/core/store/bundles/reducer.ts b/packages/sanity/src/core/store/bundles/reducer.ts index 83c8f919bea..2faf1f3b755 100644 --- a/packages/sanity/src/core/store/bundles/reducer.ts +++ b/packages/sanity/src/core/store/bundles/reducer.ts @@ -1,3 +1,5 @@ +import {DRAFTS_FOLDER, resolveBundlePerspective} from 'sanity' + import {type BundleDocument} from './types' interface BundleDeletedAction { @@ -42,6 +44,12 @@ export interface bundlesReducerState { deletedBundles: Record state: 'initialising' | 'loading' | 'loaded' | 'error' error?: Error + + /** + * An array of release ids ordered chronologically to represent the state of documents at the + * given point in time. + */ + releaseStack: string[] } function createBundlesSet(bundles: BundleDocument[] | null) { @@ -54,6 +62,7 @@ function createBundlesSet(bundles: BundleDocument[] | null) { export function bundlesReducer( state: bundlesReducerState, action: bundlesReducerAction, + perspective?: string, ): bundlesReducerState { switch (action.type) { case 'LOADING_STATE_CHANGED': { @@ -71,6 +80,10 @@ export function bundlesReducer( return { ...state, bundles: bundlesById, + releaseStack: getReleaseStack({ + bundles: bundlesById, + perspective, + }), } } @@ -82,6 +95,10 @@ export function bundlesReducer( return { ...state, bundles: currentBundles, + releaseStack: getReleaseStack({ + bundles: currentBundles, + perspective, + }), } } @@ -105,6 +122,7 @@ export function bundlesReducer( ...state, bundles: currentBundles, deletedBundles: nextDeletedBundles, + releaseStack: [...state.releaseStack].filter((id) => id !== deletedBundleId), } } @@ -117,6 +135,10 @@ export function bundlesReducer( return { ...state, bundles: currentBundles, + releaseStack: getReleaseStack({ + bundles: currentBundles, + perspective, + }), } } @@ -124,3 +146,38 @@ export function bundlesReducer( return state } } + +function getReleaseStack({ + bundles, + perspective, +}: { + bundles?: Map + perspective?: string +}): string[] { + if (typeof bundles === 'undefined') { + return [] + } + + // TODO: Handle system perspectives. + if (!perspective?.startsWith('bundle.')) { + return [] + } + + const stack = [...bundles.values()] + .toSorted(sortReleases(resolveBundlePerspective(perspective))) + .map(({_id}) => _id) + .concat(DRAFTS_FOLDER) + + return stack +} + +// TODO: Implement complete layering heuristics. +function sortReleases(perspective?: string): (a: BundleDocument, b: BundleDocument) => number { + return function (a, b) { + // Ensure the current release takes highest precedence. + if (a._id === perspective) { + return -1 + } + return 0 + } +} diff --git a/packages/sanity/src/core/store/bundles/useBundles.ts b/packages/sanity/src/core/store/bundles/useBundles.ts index e4a6cb3cb7e..98871a83022 100644 --- a/packages/sanity/src/core/store/bundles/useBundles.ts +++ b/packages/sanity/src/core/store/bundles/useBundles.ts @@ -7,10 +7,17 @@ import {type BundleDocument} from './types' interface BundlesState { data: BundleDocument[] | null + bundles: Map deletedBundles: Record error?: Error loading: boolean dispatch: React.Dispatch + + /** + * An array of release ids ordered chronologically to represent the state of documents at the + * given point in time. + */ + stack: string[] } /** @@ -24,9 +31,11 @@ export function useBundles(): BundlesState { return { data: bundlesAsArray, + bundles: state.bundles, deletedBundles, dispatch, error, loading: ['loading', 'initialising'].includes(state.state), + stack: state.releaseStack, } } 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 e8cb95844ed..bed75aeb876 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, resolveBundlePerspective} from 'sanity' +import {getPublishedId} from 'sanity' import {styled} from 'styled-components' import {type GeneralPreviewLayoutKey} from '../../../../../../../components' @@ -15,7 +15,11 @@ import { getPreviewValueWithFallback, SanityDefaultPreview, } from '../../../../../../../preview' -import {type DocumentPresence, useDocumentPreviewStore} from '../../../../../../../store' +import { + type DocumentPresence, + useBundles, + useDocumentPreviewStore, +} from '../../../../../../../store' interface SearchResultItemPreviewProps { documentId: string @@ -30,8 +34,7 @@ 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)<{$isInPerspective: boolean}>` - opacity: ${(props) => (props.$isInPerspective ? 1 : 0.5)}; +const SearchResultItemPreviewBox = styled(Box)` @supports (-webkit-overflow-scrolling: touch) { * [data-ui='Box'] { background-attachment: scroll; @@ -51,26 +54,33 @@ export function SearchResultItemPreview({ showBadge = true, }: SearchResultItemPreviewProps) { const documentPreviewStore = useDocumentPreviewStore() + const bundles = useBundles() const observable = useMemo( () => - getPreviewStateObservable( - documentPreviewStore, - schemaType, - getPublishedId(documentId), - '', - resolveBundlePerspective(perspective), - ), - [documentId, documentPreviewStore, perspective, schemaType], + getPreviewStateObservable(documentPreviewStore, schemaType, getPublishedId(documentId), '', { + bundleIds: (bundles.data ?? []).map((bundle) => bundle._id), + bundleStack: bundles.stack, + }), + [bundles.data, bundles.stack, documentId, documentPreviewStore, schemaType], ) - const {draft, published, isLoading, version} = useObservable(observable, { + const { + draft, + published, + isLoading: previewIsLoading, + version, + versions, + } = useObservable(observable, { draft: null, isLoading: true, published: null, version: null, + versions: {}, }) + const isLoading = previewIsLoading || bundles.loading + const sanityDocument = useMemo(() => { return { _id: documentId, @@ -84,15 +94,22 @@ export function SearchResultItemPreview({ {presence && presence.length > 0 && } {showBadge && {schemaType.title}} - + ) - }, [draft, isLoading, presence, published, schemaType.title, showBadge, version]) + }, [draft, isLoading, presence, published, schemaType.title, showBadge, version, versions]) - const tooltip = + const tooltip = ( + + ) return ( - + void) | null>(null) const [searchCommandList, setSearchCommandList] = useState(null) - const perspective = useRouter().stickyParams.perspective + const bundles = useBundles() const schema = useSchema() const currentUser = useCurrentUser() const { @@ -143,9 +141,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { skipSortByScore: ordering.ignoreScore, ...(ordering.sort ? {sort: [ordering.sort]} : {}), cursor: cursor || undefined, - ...resolvePerspectiveOptions(perspective, (perspectives, isSystemPerspective) => - isSystemPerspective ? perspectives : perspectives.concat(DRAFTS_FOLDER), - ), + ...resolvePerspectiveOptions(bundles.stack), }, terms: { ...terms, @@ -171,7 +167,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { searchState.terms, terms, cursor, - perspective, + bundles.stack, ]) /** diff --git a/packages/sanity/src/core/util/resolvePerspective.ts b/packages/sanity/src/core/util/resolvePerspective.ts index 1801d09f900..1228e142ac0 100644 --- a/packages/sanity/src/core/util/resolvePerspective.ts +++ b/packages/sanity/src/core/util/resolvePerspective.ts @@ -8,35 +8,28 @@ export function resolveBundlePerspective(perspective?: string): string | undefin return perspective?.split(/^bundle./).at(1) } +// TODO: Improve handling of this scenario. +const MAXIMUM_SUPPORTED_BUNDLE_PERSPECTIVES = 10 + /** * 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. + * an object with a `bundlePerspective` property 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 { + perspective: string[] | undefined, +): {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(','), + // TODO: The slice operation is to ensure we don't send an invalid request to Content Lake. + // In production, it shouldn't be possible for a project to exceed this quantity of + // releases in the first place. We should improve handling of this scenario and remove + // the slice operation to avoid unexpected behaviour. + bundlePerspective: perspective.slice(0, MAXIMUM_SUPPORTED_BUNDLE_PERSPECTIVES - 1).join(','), } } diff --git a/packages/sanity/src/structure/components/paneItem/PaneItem.tsx b/packages/sanity/src/structure/components/paneItem/PaneItem.tsx index ee178d992f7..52e6c262039 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItem.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItem.tsx @@ -77,7 +77,7 @@ export function PaneItem(props: PaneItemProps) { const schema = useSchema() const documentPreviewStore = useDocumentPreviewStore() const {ChildLink} = usePaneRouter() - const perspective = useRouter().stickyParams.perspective + const {perspective} = useRouter().perspectiveState const documentPresence = useDocumentPresence(id) const hasSchemaType = Boolean(schemaType && schemaType.name && schema.get(schemaType.name)) const [clicked, setClicked] = useState(false) diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx index 0a88a37db78..a049a89da63 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx @@ -13,17 +13,12 @@ import { getPreviewStateObservable, getPreviewValueWithFallback, isRecord, - resolveBundlePerspective, SanityDefaultPreview, + useBundles, } from 'sanity' -import {styled} from 'styled-components' import {TooltipDelayGroupProvider} from '../../../ui-components' -const Root = styled.div<{$isInPerspective: boolean}>` - opacity: ${(props) => (props.$isInPerspective ? 1 : 0.5)}; -` - export interface PaneItemPreviewProps { documentPreviewStore: DocumentPreviewStore icon: ComponentType | false @@ -50,47 +45,60 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { ? value.title : null + const bundles = useBundles() + const previewStateObservable = useMemo( () => - getPreviewStateObservable( - props.documentPreviewStore, - schemaType, - value._id, - title, - resolveBundlePerspective(perspective), - ), - [props.documentPreviewStore, schemaType, title, value._id, perspective], + getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title, { + bundleIds: (bundles.data ?? []).map((bundle) => bundle._id), + bundleStack: bundles.stack, + }), + [props.documentPreviewStore, schemaType, value._id, title, bundles.data, bundles.stack], ) - const {draft, published, version, isLoading} = useObservable(previewStateObservable, { + const { + draft, + published, + version, + versions, + isLoading: previewIsLoading, + } = useObservable(previewStateObservable, { draft: null, isLoading: true, published: null, version: null, + versions: {}, perspective, }) + const isLoading = previewIsLoading || bundles.loading + const status = isLoading ? null : ( {presence && presence.length > 0 && } - + ) - const tooltip = + const tooltip = ( + + ) return ( - - - + ) } diff --git a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx index 6f16c5b5269..0e19e5e9223 100644 --- a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx +++ b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx @@ -5,12 +5,12 @@ import {useObservableEvent} from 'react-rx' import {debounce, map, type Observable, of, tap, timer} from 'rxjs' import { type GeneralPreviewLayoutKey, + useBundles, useI18nText, useSchema, useTranslation, useUnique, } from 'sanity' -import {useRouter} from 'sanity/router' import {keyframes, styled} from 'styled-components' import {structureLocaleNamespace} from '../../i18n' @@ -74,9 +74,7 @@ const DelayedSubtleSpinnerIcon = styled(SpinnerIcon)` export const DocumentListPane = memo(function DocumentListPane(props: DocumentListPaneProps) { const {childItemId, isActive, pane, paneKey, sortOrder: sortOrderRaw, layout} = props const schema = useSchema() - - const perspective = useRouter().stickyParams.perspective - + const bundles = useBundles() const {displayOptions, options} = pane const {apiVersion, filter} = options const params = useShallowUnique(options.params || EMPTY_RECORD) @@ -102,15 +100,24 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi const sortOrder = useUnique(sortWithOrderingFn) - const {error, isLoadingFullList, isLoading, items, fromCache, onLoadFullList, onRetry} = - useDocumentList({ - apiVersion, - filter, - perspective, - params, - searchQuery: searchQuery?.trim(), - sortOrder, - }) + const { + error, + isLoadingFullList, + isLoading: documentListIsLoading, + items, + fromCache, + onLoadFullList, + onRetry, + } = useDocumentList({ + apiVersion, + filter, + perspective: bundles.stack, + params, + searchQuery: searchQuery?.trim(), + sortOrder, + }) + + const isLoading = documentListIsLoading || bundles.loading const handleQueryChange = useObservableEvent( (event$: Observable>) => { diff --git a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts index 4d0f5fbe54c..5611c4b4fe6 100644 --- a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts +++ b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts @@ -18,7 +18,6 @@ import {exhaustMapWithTrailing} from 'rxjs-exhaustmap-with-trailing' import { createSearch, createSWR, - DRAFTS_FOLDER, getSearchableTypes, resolvePerspectiveOptions, type SanityDocumentLike, @@ -37,7 +36,7 @@ interface ListenQueryOptions { schema: Schema searchQuery: string sort: SortOrder - perspective?: string + perspective?: string[] staticTypeNames?: string[] | null maxFieldDepth?: number enableLegacySearch?: boolean @@ -152,9 +151,7 @@ export function listenSearchQuery(options: ListenQueryOptions): Observable - isSystemPerspective ? perspectives : perspectives.concat(DRAFTS_FOLDER), - ), + ...resolvePerspectiveOptions(perspective), } return search(searchTerms, searchOptions).pipe( diff --git a/packages/sanity/src/structure/panes/documentList/useDocumentList.ts b/packages/sanity/src/structure/panes/documentList/useDocumentList.ts index 23c9ace3875..7f82638ef19 100644 --- a/packages/sanity/src/structure/panes/documentList/useDocumentList.ts +++ b/packages/sanity/src/structure/panes/documentList/useDocumentList.ts @@ -30,7 +30,7 @@ import {type DocumentListPaneItem, type SortOrder} from './types' interface UseDocumentListOpts { apiVersion?: string filter: string - perspective?: string + perspective?: string[] params: Record searchQuery: string | null sortOrder?: SortOrder