From 81c5fc370cf592199d22da38b6b9e6ada076c6dc Mon Sep 17 00:00:00 2001 From: Pedro Bonamin <46196328+pedrobonamin@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:47:26 +0100 Subject: [PATCH] feat(core): move bundlesPerspective to usePerspective hook, sort releases (#7682) --- .../core/releases/components/ReleasesMenu.tsx | 5 +- .../releases/hooks/__tests__/utils.test.ts | 232 ++++++++++++++++++ .../core/releases/hooks/usePerspective.tsx | 25 +- .../sanity/src/core/releases/hooks/utils.ts | 77 ++++++ .../releases/navbar/GlobalPerspectiveMenu.tsx | 26 +- .../tool/detail/AddDocumentSearch.tsx | 5 +- .../src/core/store/_legacy/datastores.ts | 15 +- .../core/store/release/createReleaseStore.ts | 7 +- .../sanity/src/core/store/release/reducer.ts | 65 ----- .../sanity/src/core/store/release/types.ts | 6 + .../src/core/store/release/useReleases.ts | 32 ++- .../item/SearchResultItemPreview.tsx | 6 +- .../search/contexts/search/SearchProvider.tsx | 9 +- .../components/paneItem/PaneItemPreview.tsx | 7 +- .../paneRouter/PaneRouterProvider.tsx | 1 + .../structure/components/paneRouter/types.ts | 1 + .../perspective/DocumentPerspectiveList.tsx | 7 +- .../document/statusBar/DocumentStatusLine.tsx | 4 +- .../panes/documentList/DocumentListPane.tsx | 4 +- 19 files changed, 391 insertions(+), 143 deletions(-) create mode 100644 packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts create mode 100644 packages/sanity/src/core/releases/hooks/utils.ts diff --git a/packages/sanity/src/core/releases/components/ReleasesMenu.tsx b/packages/sanity/src/core/releases/components/ReleasesMenu.tsx index 5a5eec4ab37..e9afcda7046 100644 --- a/packages/sanity/src/core/releases/components/ReleasesMenu.tsx +++ b/packages/sanity/src/core/releases/components/ReleasesMenu.tsx @@ -25,15 +25,14 @@ interface BundleListProps { releases: ReleaseDocument[] | null loading: boolean actions?: ReactElement - perspective?: string } /** * @internal */ export const ReleasesMenu = memo(function ReleasesMenu(props: BundleListProps): ReactElement { - const {releases, loading, actions, button, perspective} = props - const {currentGlobalBundle, setPerspectiveFromRelease} = usePerspective(perspective) + const {releases, loading, actions, button} = props + const {currentGlobalBundle, setPerspectiveFromRelease} = usePerspective() const {t} = useTranslation() const sortedBundlesToDisplay = useMemo(() => { diff --git a/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts b/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts new file mode 100644 index 00000000000..aa21af9ca8b --- /dev/null +++ b/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts @@ -0,0 +1,232 @@ +import {describe, expect, it} from 'vitest' + +import {RELEASE_DOCUMENT_TYPE} from '../../../store/release/constants' +import {type ReleaseDocument} from '../../../store/release/types' +import {createReleaseId} from '../../util/createReleaseId' +import {getBundleIdFromReleaseId} from '../../util/getBundleIdFromReleaseId' +import {getReleasesPerspective, sortReleases} from '../utils' + +function createReleaseMock( + value: Partial< + Omit & { + metadata: Partial + } + >, +): ReleaseDocument { + const id = value._id || createReleaseId() + const name = getBundleIdFromReleaseId(id) + return { + _id: id, + _type: RELEASE_DOCUMENT_TYPE, + _createdAt: new Date().toISOString(), + _updatedAt: new Date().toISOString(), + name: getBundleIdFromReleaseId(id), + createdBy: 'snty1', + state: 'active', + ...value, + metadata: { + title: `Release ${name}`, + releaseType: 'asap', + ...value.metadata, + }, + } +} +describe('sortReleases()', () => { + it('should return the asap releases ordered by createdAt', () => { + const releases: ReleaseDocument[] = [ + createReleaseMock({ + _id: '_.releases.asap1', + _createdAt: '2024-10-24T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + createReleaseMock({ + _id: '_.releases.asap2', + _createdAt: '2024-10-25T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + ] + const sorted = sortReleases(releases) + const expectedOrder = ['asap2', 'asap1'] + expectedOrder.forEach((expectedName, idx) => { + expect(sorted[idx].name).toBe(expectedName) + }) + }) + it('should return the scheduled releases ordered by intendedPublishAt or publishAt', () => { + const releases: ReleaseDocument[] = [ + createReleaseMock({ + _id: '_.releases.future2', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-11-25T00:00:00Z', + }, + }), + createReleaseMock({ + _id: '_.releases.future1', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-11-23T00:00:00Z', + }, + }), + createReleaseMock({ + _id: '_.releases.future4', + state: 'scheduled', + publishAt: '2024-11-31T00:00:00Z', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-10-20T00:00:00Z', + }, + }), + createReleaseMock({ + _id: '_.releases.future3', + state: 'scheduled', + publishAt: '2024-11-26T00:00:00Z', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-11-22T00:00:00Z', + }, + }), + ] + const sorted = sortReleases(releases) + const expectedOrder = ['future4', 'future3', 'future2', 'future1'] + expectedOrder.forEach((expectedName, idx) => { + expect(sorted[idx].name).toBe(expectedName) + }) + }) + it('should return the undecided releases ordered by createdAt', () => { + const releases: ReleaseDocument[] = [ + createReleaseMock({ + _id: '_.releases.undecided1', + _createdAt: '2024-10-25T00:00:00Z', + metadata: { + releaseType: 'undecided', + }, + }), + createReleaseMock({ + _id: '_.releases.undecided2', + _createdAt: '2024-10-26T00:00:00Z', + metadata: { + releaseType: 'undecided', + }, + }), + ] + const sorted = sortReleases(releases) + const expectedOrder = ['undecided2', 'undecided1'] + expectedOrder.forEach((expectedName, idx) => { + expect(sorted[idx].name).toBe(expectedName) + }) + }) + it("should gracefully combine all release types, and sort them by 'undecided', 'scheduled', 'asap'", () => { + const releases = [ + createReleaseMock({ + _id: '_.releases.asap2', + _createdAt: '2024-10-25T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + createReleaseMock({ + _id: '_.releases.asap1', + _createdAt: '2024-10-24T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + createReleaseMock({ + _id: '_.releases.undecided2', + _createdAt: '2024-10-26T00:00:00Z', + metadata: { + releaseType: 'undecided', + }, + }), + createReleaseMock({ + _id: '_.releases.future4', + state: 'scheduled', + publishAt: '2024-11-31T00:00:00Z', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-10-20T00:00:00Z', + }, + }), + createReleaseMock({ + _id: '_.releases.future1', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-11-23T00:00:00Z', + }, + }), + ] + const sorted = sortReleases(releases) + const expectedOrder = ['undecided2', 'future4', 'future1', 'asap2', 'asap1'] + expectedOrder.forEach((expectedName, idx) => { + expect(sorted[idx].name).toBe(expectedName) + }) + }) +}) + +describe('getReleasesPerspective()', () => { + const releases = [ + createReleaseMock({ + _id: '_.releases.asap2', + _createdAt: '2024-10-25T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + createReleaseMock({ + _id: '_.releases.asap1', + _createdAt: '2024-10-24T00:00:00Z', + metadata: { + releaseType: 'asap', + }, + }), + createReleaseMock({ + _id: '_.releases.undecided2', + _createdAt: '2024-10-26T00:00:00Z', + metadata: { + releaseType: 'undecided', + }, + }), + createReleaseMock({ + _id: '_.releases.future4', + state: 'scheduled', + publishAt: '2024-11-31T00:00:00Z', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-10-20T00:00:00Z', + }, + }), + createReleaseMock({ + _id: '_.releases.future1', + metadata: { + releaseType: 'scheduled', + intendedPublishAt: '2024-11-23T00:00:00Z', + }, + }), + ] + // Define your test cases with the expected outcomes + const testCases = [ + {perspective: 'bundle.asap1', excluded: [], expected: ['asap1', 'drafts']}, + {perspective: 'bundle.asap2', excluded: [], expected: ['asap2', 'asap1', 'drafts']}, + { + perspective: 'bundle.undecided2', + excluded: [], + expected: ['undecided2', 'future4', 'future1', 'asap2', 'asap1', 'drafts'], + }, + { + perspective: 'bundle.undecided2', + excluded: ['future1', 'drafts'], + expected: ['undecided2', 'future4', 'asap2', 'asap1'], + }, + ] + it.each(testCases)( + 'should return the correct release stack for %s', + ({perspective, excluded, expected}) => { + const result = getReleasesPerspective({releases, perspective, excluded}) + expect(result).toEqual(expected) + }, + ) +}) diff --git a/packages/sanity/src/core/releases/hooks/usePerspective.tsx b/packages/sanity/src/core/releases/hooks/usePerspective.tsx index 9b2ac9e42be..ed76e9cb30c 100644 --- a/packages/sanity/src/core/releases/hooks/usePerspective.tsx +++ b/packages/sanity/src/core/releases/hooks/usePerspective.tsx @@ -1,9 +1,11 @@ +import {useMemo} from 'react' import {useRouter} from 'sanity/router' import {type ReleaseType, useReleases} from '../../store/release' import {type ReleaseDocument} from '../../store/release/types' import {LATEST} from '../util/const' import {getBundleIdFromReleaseId} from '../util/getBundleIdFromReleaseId' +import {getReleasesPerspective} from './utils' /** * @internal @@ -21,6 +23,10 @@ export interface PerspectiveValue { setPerspective: (releaseId: string) => void /* change the perspective in the studio based on a release ID */ setPerspectiveFromRelease: (releaseId: string) => void + /** + * The stacked array of releases ids ordered chronologically to represent the state of documents at the given point in time. + */ + bundlesPerspective: string[] } /** @@ -28,10 +34,10 @@ export interface PerspectiveValue { * * @internal */ -export function usePerspective(selectedPerspective?: string): PerspectiveValue { +export function usePerspective(): PerspectiveValue { const router = useRouter() - const {data: releases, dispatch} = useReleases() - const perspective = selectedPerspective ?? router.stickyParams.perspective + const {data: releases} = useReleases() + const perspective = router.stickyParams.perspective // TODO: Should it be possible to set the perspective within a pane, rather than globally? const setPerspective = (releaseId: string | undefined) => { @@ -44,7 +50,6 @@ export function usePerspective(selectedPerspective?: string): PerspectiveValue { } router.navigateStickyParam('perspective', perspectiveParam) - dispatch({type: 'PERSPECTIVE_SET', payload: perspectiveParam}) } const selectedBundle = @@ -69,9 +74,21 @@ export function usePerspective(selectedPerspective?: string): PerspectiveValue { const setPerspectiveFromRelease = (releaseId: string) => setPerspective(getBundleIdFromReleaseId(releaseId)) + const bundlesPerspective = useMemo( + () => + getReleasesPerspective({ + releases, + perspective, + // TODO: Implement excluded perspectives + excluded: [], + }), + [releases, perspective], + ) + return { setPerspective, setPerspectiveFromRelease, currentGlobalBundle: currentGlobalBundle, + bundlesPerspective, } } diff --git a/packages/sanity/src/core/releases/hooks/utils.ts b/packages/sanity/src/core/releases/hooks/utils.ts new file mode 100644 index 00000000000..a6bff50cf16 --- /dev/null +++ b/packages/sanity/src/core/releases/hooks/utils.ts @@ -0,0 +1,77 @@ +import {type ReleaseDocument} from '../../store/release/types' +import {DRAFTS_FOLDER} from '../../util/draftUtils' +import {resolveBundlePerspective} from '../../util/resolvePerspective' +import {getBundleIdFromReleaseId} from '../util/getBundleIdFromReleaseId' + +export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[] { + // The order should always be: + // [undecided (sortByCreatedAt), scheduled(sortBy publishAt || metadata.intendedPublishAt), asap(sortByCreatedAt)] + return [...releases].sort((a, b) => { + // undecided are always first, then by createdAt descending + if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType !== 'undecided') { + return -1 + } + if (a.metadata.releaseType !== 'undecided' && b.metadata.releaseType === 'undecided') { + return 1 + } + if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType === 'undecided') { + // Sort by createdAt + return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime() + } + + // Scheduled are always at the middle, then by publishAt descending + if (a.metadata.releaseType === 'scheduled' && b.metadata.releaseType === 'scheduled') { + const aPublishAt = a.publishAt || a.metadata.intendedPublishAt + if (!aPublishAt) { + return 1 + } + const bPublishAt = b.publishAt || b.metadata.intendedPublishAt + if (!bPublishAt) { + return -1 + } + return new Date(bPublishAt).getTime() - new Date(aPublishAt).getTime() + } + + // ASAP are always last, then by createdAt descending + if (a.metadata.releaseType === 'asap' && b.metadata.releaseType !== 'asap') { + return 1 + } + if (a.metadata.releaseType !== 'asap' && b.metadata.releaseType === 'asap') { + return -1 + } + if (a.metadata.releaseType === 'asap' && b.metadata.releaseType === 'asap') { + // Sort by createdAt + return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime() + } + + return 0 + }) +} + +export function getReleasesPerspective({ + releases, + perspective, + excluded, +}: { + releases: ReleaseDocument[] + perspective: string | undefined // Includes the bundle. or 'published' + excluded: string[] +}): string[] { + if (!perspective?.startsWith('bundle.')) { + return [] + } + const perspectiveId = resolveBundlePerspective(perspective) + if (!perspectiveId) { + return [] + } + + const sorted = sortReleases(releases).map((release) => getBundleIdFromReleaseId(release._id)) + const selectedIndex = sorted.indexOf(perspectiveId) + if (selectedIndex === -1) { + return [] + } + return sorted + .slice(selectedIndex) + .concat(DRAFTS_FOLDER) + .filter((name) => !excluded.includes(name)) +} diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index 01ade74c314..1efe8f789b7 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -1,7 +1,6 @@ import {AddIcon, ChevronDownIcon} from '@sanity/icons' // eslint-disable-next-line no-restricted-imports -- MenuItem requires props, only supported by @sanity/ui import {Box, Button, Flex, Menu, MenuDivider, MenuItem, Spinner} from '@sanity/ui' -import {compareDesc} from 'date-fns' import {useCallback, useMemo, useRef, useState} from 'react' import {type ReleaseDocument, type ReleaseType} from 'sanity' import {styled} from 'styled-components' @@ -11,7 +10,6 @@ import {useTranslation} from '../../i18n' import {useReleases} from '../../store/release/useReleases' import {ReleaseDetailsDialog} from '../components/dialog/ReleaseDetailsDialog' import {usePerspective} from '../hooks' -import {getPublishDateFromRelease} from '../util/util' import { getRangePosition, GlobalPerspectiveMenuItem, @@ -20,8 +18,6 @@ import { import {ReleaseTypeMenuSection} from './ReleaseTypeMenuSection' import {useScrollIndicatorVisibility} from './useScrollIndicatorVisibility' -type ReleaseTypeSort = (a: ReleaseDocument, b: ReleaseDocument) => number - const StyledMenu = styled(Menu)` min-width: 200px; max-width: 320px; @@ -40,17 +36,6 @@ const StyledPublishedBox = styled(Box)` padding-bottom: 16px; ` -const sortReleaseByPublishAt: ReleaseTypeSort = (ARelease, BRelease) => - compareDesc(getPublishDateFromRelease(BRelease), getPublishDateFromRelease(ARelease)) -const sortReleaseByTitle: ReleaseTypeSort = (ARelease, BRelease) => - ARelease.metadata.title.localeCompare(BRelease.metadata.title) - -const releaseTypeSorting: Record = { - asap: sortReleaseByTitle, - scheduled: sortReleaseByPublishAt, - undecided: sortReleaseByTitle, -} - const orderedReleaseTypes: ReleaseType[] = ['asap', 'scheduled', 'undecided'] const ASAP_RANGE_OFFSET = 2 @@ -76,23 +61,16 @@ export function GlobalPerspectiveMenu(): JSX.Element { setCreateBundleDialogOpen(false) }, []) - const unarchivedReleases = useMemo( - () => releases.filter((release) => release.state !== 'archived'), - [releases], - ) - const sortedReleaseTypeReleases = useMemo( () => orderedReleaseTypes.reduce>( (ReleaseTypeReleases, releaseType) => ({ ...ReleaseTypeReleases, - [releaseType]: unarchivedReleases - .filter(({metadata}) => metadata.releaseType === releaseType) - .sort(releaseTypeSorting[releaseType]), + [releaseType]: releases.filter(({metadata}) => metadata.releaseType === releaseType), }), {} as Record, ), - [unarchivedReleases], + [releases], ) const range: LayerRange = useMemo(() => { diff --git a/packages/sanity/src/core/releases/tool/detail/AddDocumentSearch.tsx b/packages/sanity/src/core/releases/tool/detail/AddDocumentSearch.tsx index 7aa189eba63..82fd1f21f00 100644 --- a/packages/sanity/src/core/releases/tool/detail/AddDocumentSearch.tsx +++ b/packages/sanity/src/core/releases/tool/detail/AddDocumentSearch.tsx @@ -1,10 +1,11 @@ +import {type SanityDocumentLike} from '@sanity/types' import {LayerProvider, PortalProvider, useToast} from '@sanity/ui' import {useCallback} from 'react' -import {getBundleIdFromReleaseId, useReleaseOperations} from 'sanity' -import {type SanityDocumentLike} from '../../../../../../@sanity/types/src/documents/types' +import {useReleaseOperations} from '../../../store/release/useReleaseOperations' import {SearchPopover} from '../../../studio/components/navbar/search/components/SearchPopover' import {SearchProvider} from '../../../studio/components/navbar/search/contexts/search/SearchProvider' +import {getBundleIdFromReleaseId} from '../../util/getBundleIdFromReleaseId' export function AddDocumentSearch({ open, diff --git a/packages/sanity/src/core/store/_legacy/datastores.ts b/packages/sanity/src/core/store/_legacy/datastores.ts index 0254846eaf3..4dfc92a2102 100644 --- a/packages/sanity/src/core/store/_legacy/datastores.ts +++ b/packages/sanity/src/core/store/_legacy/datastores.ts @@ -1,9 +1,8 @@ /* eslint-disable camelcase */ import {useTelemetry} from '@sanity/telemetry/react' -import {useCallback, useMemo, useRef} from 'react' +import {useCallback, useMemo} from 'react' import {of} from 'rxjs' -import {useRouter} from 'sanity/router' import {useClient, useSchema, useTemplates} from '../../hooks' import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../preview' @@ -297,32 +296,26 @@ export function useKeyValueStore(): KeyValueStore { export function useReleasesStore(): ReleaseStore { const resourceCache = useResourceCache() const workspace = useWorkspace() - const currentUser = useCurrentUser() const previewStore = useDocumentPreviewStore() const studioClient = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) - const router = useRouter() - const perspectiveRef = useRef(router.perspectiveState.perspective) - // TODO: Include hidden layers state. return useMemo(() => { const releaseStore = resourceCache.get({ - dependencies: [workspace, currentUser, previewStore], + dependencies: [workspace, previewStore], namespace: 'ReleasesStore', }) || createReleaseStore({ client: studioClient, - currentUser, - perspective: perspectiveRef.current, previewStore, }) resourceCache.set({ - dependencies: [workspace, currentUser, previewStore], + dependencies: [workspace, previewStore], namespace: 'ReleasesStore', value: releaseStore, }) return releaseStore - }, [resourceCache, workspace, studioClient, currentUser, previewStore]) + }, [resourceCache, workspace, studioClient, previewStore]) } diff --git a/packages/sanity/src/core/store/release/createReleaseStore.ts b/packages/sanity/src/core/store/release/createReleaseStore.ts index b8815fa0f8c..b7ba8be0a91 100644 --- a/packages/sanity/src/core/store/release/createReleaseStore.ts +++ b/packages/sanity/src/core/store/release/createReleaseStore.ts @@ -1,5 +1,4 @@ import {type ListenEvent, type ListenOptions, type SanityClient} from '@sanity/client' -import {type User} from '@sanity/types' import { BehaviorSubject, catchError, @@ -70,10 +69,8 @@ const INITIAL_STATE: ReleasesReducerState = { export function createReleaseStore(context: { previewStore: DocumentPreviewStore client: SanityClient - currentUser: User | null - perspective?: string }): ReleaseStore { - const {client, currentUser, previewStore} = context + const {client, previewStore} = context const dispatch$ = new Subject() const fetchPending$ = new BehaviorSubject(false) @@ -159,7 +156,7 @@ export function createReleaseStore(context: { const state$ = merge(listFetch$, dispatch$).pipe( filter((action): action is ReleasesReducerAction => typeof action !== 'undefined'), - scan((state, action) => releasesReducer(state, action, context.perspective), INITIAL_STATE), + scan((state, action) => releasesReducer(state, action), INITIAL_STATE), startWith(INITIAL_STATE), shareReplay(1), ) diff --git a/packages/sanity/src/core/store/release/reducer.ts b/packages/sanity/src/core/store/release/reducer.ts index fdc1291df2f..6684cae628c 100644 --- a/packages/sanity/src/core/store/release/reducer.ts +++ b/packages/sanity/src/core/store/release/reducer.ts @@ -1,5 +1,3 @@ -import {DRAFTS_FOLDER, getBundleIdFromReleaseId, resolveBundlePerspective} from 'sanity' - import {type ReleaseDocument} from './types' interface BundleDeletedAction { @@ -32,18 +30,12 @@ interface LoadingStateChangedAction { type: 'LOADING_STATE_CHANGED' } -interface PerspectiveSetAction { - payload: string - type: 'PERSPECTIVE_SET' -} - export type ReleasesReducerAction = | BundleDeletedAction | BundleUpdatedAction | ReleasesSetAction | BundleReceivedAction | LoadingStateChangedAction - | PerspectiveSetAction export interface ReleasesReducerState { releases: Map @@ -67,18 +59,8 @@ function createReleasesSet(releases: ReleaseDocument[] | null) { export function releasesReducer( state: ReleasesReducerState, action: ReleasesReducerAction, - perspective?: string, ): ReleasesReducerState { switch (action.type) { - case 'PERSPECTIVE_SET': { - return { - ...state, - releaseStack: getReleaseStack({ - releases: state.releases, - perspective: action.payload, - }), - } - } case 'LOADING_STATE_CHANGED': { return { ...state, @@ -94,10 +76,6 @@ export function releasesReducer( return { ...state, releases: releasesById, - releaseStack: getReleaseStack({ - releases: releasesById, - perspective, - }), } } @@ -109,10 +87,6 @@ export function releasesReducer( return { ...state, releases: currentReleases, - releaseStack: getReleaseStack({ - releases: currentReleases, - perspective, - }), } } @@ -125,10 +99,6 @@ export function releasesReducer( return { ...state, releases: currentReleases, - releaseStack: getReleaseStack({ - releases: currentReleases, - perspective, - }), } } @@ -136,38 +106,3 @@ export function releasesReducer( return state } } - -function getReleaseStack({ - releases, - perspective, -}: { - releases?: Map - perspective?: string -}): string[] { - if (typeof releases === 'undefined') { - return [] - } - - // TODO: Handle system perspectives. - if (!perspective?.startsWith('bundle.')) { - return [] - } - - const stack = [...releases.values()] - .toSorted(sortReleases(resolveBundlePerspective(perspective))) - .map(({_id}) => getBundleIdFromReleaseId(_id)) - .concat(DRAFTS_FOLDER) - - return stack -} - -// TODO: Implement complete layering heuristics. -function sortReleases(perspective?: string): (a: ReleaseDocument, b: ReleaseDocument) => number { - return function (a, b) { - // Ensure the current release takes highest precedence. - if (getBundleIdFromReleaseId(a._id) === perspective) { - return -1 - } - return 0 - } -} diff --git a/packages/sanity/src/core/store/release/types.ts b/packages/sanity/src/core/store/release/types.ts index d51ed9a8b27..90484146fb4 100644 --- a/packages/sanity/src/core/store/release/types.ts +++ b/packages/sanity/src/core/store/release/types.ts @@ -46,10 +46,16 @@ export interface ReleaseDocument { _type: typeof RELEASE_DOCUMENT_TYPE _createdAt: string _updatedAt: string + /** + * The same as the last path segment of the _id, added by the backend. + */ name: string createdBy: string state: ReleaseState finalDocumentStates?: ReleaseFinalDocumentState[] + /** + * If defined, it takes precedence over the intendedPublishAt, the state should be 'scheduled' + */ publishAt?: string metadata: { title: string diff --git a/packages/sanity/src/core/store/release/useReleases.ts b/packages/sanity/src/core/store/release/useReleases.ts index 6fa16ab00f3..387c36c3c56 100644 --- a/packages/sanity/src/core/store/release/useReleases.ts +++ b/packages/sanity/src/core/store/release/useReleases.ts @@ -1,22 +1,23 @@ import {useMemo} from 'react' import {useObservable} from 'react-rx' +import {sortReleases} from '../../releases/hooks/utils' import {useReleasesStore} from '../_legacy/datastores' import {type ReleasesReducerAction} from './reducer' import {type ReleaseDocument} from './types' interface ReleasesState { + /** + * Sorted array of releases, excluding archived releases + */ data: ReleaseDocument[] - // releases: Map - error?: Error - loading: boolean - dispatch: (action: ReleasesReducerAction) => void - /** - * An array of release ids ordered chronologically to represent the state of documents at the - * given point in time. + * Array of archived releases */ - stack: string[] + archivedReleases: ReleaseDocument[] + error?: Error + loading: boolean + dispatch: (event: ReleasesReducerAction) => void } /** @@ -25,13 +26,22 @@ interface ReleasesState { export function useReleases(): ReleasesState { const {state$, dispatch} = useReleasesStore() const state = useObservable(state$)! - const releasesAsArray = useMemo(() => Array.from(state.releases.values()), [state.releases]) - + const releasesAsArray = useMemo( + () => + sortReleases( + Array.from(state.releases.values()).filter((release) => release.state !== 'archived'), + ).reverse(), + [state.releases], + ) + const archivedReleases = useMemo( + () => Array.from(state.releases.values()).filter((release) => release.state === 'archived'), + [state.releases], + ) return { data: releasesAsArray, + archivedReleases, dispatch, error: state.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 2eb09d08e8d..10f68980e90 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 @@ -15,6 +15,7 @@ import { getPreviewValueWithFallback, SanityDefaultPreview, } from '../../../../../../../preview' +import {usePerspective} from '../../../../../../../releases/hooks/usePerspective' import { type DocumentPresence, useDocumentPreviewStore, @@ -55,14 +56,15 @@ export function SearchResultItemPreview({ }: SearchResultItemPreviewProps) { const documentPreviewStore = useDocumentPreviewStore() const releases = useReleases() + const {bundlesPerspective} = usePerspective() const observable = useMemo( () => getPreviewStateObservable(documentPreviewStore, schemaType, getPublishedId(documentId), '', { bundleIds: (releases.data ?? []).map((release) => getBundleIdFromReleaseId(release._id)), - bundleStack: releases.stack, + bundleStack: bundlesPerspective, }), - [releases.data, releases.stack, documentId, documentPreviewStore, schemaType], + [releases.data, bundlesPerspective, documentId, documentPreviewStore, schemaType], ) const { 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 96caf9f46e3..ee9465737ac 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 @@ -4,8 +4,9 @@ import {SearchContext} from 'sanity/_singletons' import {type CommandListHandle} from '../../../../../../components' import {useSchema} from '../../../../../../hooks' +import {usePerspective} from '../../../../../../releases/hooks/usePerspective' import {type SearchTerms} from '../../../../../../search' -import {useCurrentUser, useReleases} from '../../../../../../store' +import {useCurrentUser} from '../../../../../../store' import {resolvePerspectiveOptions} from '../../../../../../util/resolvePerspective' import {useSource} from '../../../../../source' import {SEARCH_LIMIT} from '../../constants' @@ -31,7 +32,7 @@ interface SearchProviderProps { export function SearchProvider({children, fullscreen}: SearchProviderProps) { const [onClose, setOnClose] = useState<(() => void) | null>(null) const [searchCommandList, setSearchCommandList] = useState(null) - const releases = useReleases() + const {bundlesPerspective} = usePerspective() const schema = useSchema() const currentUser = useCurrentUser() const { @@ -141,7 +142,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { skipSortByScore: ordering.ignoreScore, ...(ordering.sort ? {sort: [ordering.sort]} : {}), cursor: cursor || undefined, - ...resolvePerspectiveOptions(releases.stack), + ...resolvePerspectiveOptions(bundlesPerspective), }, terms: { ...terms, @@ -167,7 +168,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { searchState.terms, terms, cursor, - releases.stack, + bundlesPerspective, ]) /** diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx index 063a71ea7b8..0564599c7e0 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx @@ -15,6 +15,7 @@ import { getPreviewValueWithFallback, isRecord, SanityDefaultPreview, + usePerspective, useReleases, } from 'sanity' @@ -47,14 +48,14 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { : null const releases = useReleases() - + const {bundlesPerspective} = usePerspective() const previewStateObservable = useMemo( () => getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title, { bundleIds: (releases.data ?? []).map((release) => getBundleIdFromReleaseId(release._id)), - bundleStack: releases.stack, + bundleStack: bundlesPerspective, }), - [props.documentPreviewStore, schemaType, value._id, title, releases.data, releases.stack], + [props.documentPreviewStore, schemaType, value._id, title, releases.data, bundlesPerspective], ) const { diff --git a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx index 94356a57cad..a120c430c62 100644 --- a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx +++ b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx @@ -24,6 +24,7 @@ export function PaneRouterProvider(props: { params: Record payload: unknown siblingIndex: number + // TODO: Remove this paneRouter perspective - releases are only global perspective?: string }) { const {children, flatIndex, index, params, payload, siblingIndex} = props diff --git a/packages/sanity/src/structure/components/paneRouter/types.ts b/packages/sanity/src/structure/components/paneRouter/types.ts index b153f876a34..1780b6c9b32 100644 --- a/packages/sanity/src/structure/components/paneRouter/types.ts +++ b/packages/sanity/src/structure/components/paneRouter/types.ts @@ -170,6 +170,7 @@ export interface PaneRouterContextValue { ) => void /** + * TODO: Remove this paneRouter perspective - releases are only global * Perspective of the current pane */ perspective?: string diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx index b266469d734..47e0be37f3e 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx @@ -19,7 +19,7 @@ import {VersionChip} from './VersionChip' export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() { const {perspective} = usePaneRouter() const {t} = useTranslation() - const {setPerspective} = usePerspective(perspective) + const {setPerspective} = usePerspective() const dateTimeFormat = useDateTimeFormat({ dateStyle: 'medium', timeStyle: 'short', @@ -41,10 +41,7 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() { // remove the archived releases const filteredReleases = (documentVersions && - releases?.filter( - (release) => - !versionDocumentExists(documentVersions, release._id) && release.state !== 'archived', - )) || + releases?.filter((release) => !versionDocumentExists(documentVersions, release._id))) || [] const asapReleases = documentVersions?.filter( diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx index 79976d6112d..4c3cd9a0f19 100644 --- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx +++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx @@ -3,7 +3,6 @@ import {useEffect, useLayoutEffect, useState} from 'react' import {DocumentStatus, DocumentStatusIndicator, usePerspective, useSyncState} from 'sanity' import {Tooltip} from '../../../../ui-components' -import {usePaneRouter} from '../../../components/paneRouter' import {useDocumentPane} from '../useDocumentPane' import {DocumentStatusPulse} from './DocumentStatusPulse' @@ -20,8 +19,7 @@ export function DocumentStatusLine({singleLine}: DocumentStatusLineProps) { const [status, setStatus] = useState<'saved' | 'syncing' | null>(null) const syncState = useSyncState(documentId, documentType, {version: editState?.bundleId}) - const paneRouter = usePaneRouter() - const {currentGlobalBundle} = usePerspective(paneRouter.perspective) + const {currentGlobalBundle} = usePerspective() const lastUpdated = value?._updatedAt diff --git a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx index effbefa6d1f..48c1cca7a50 100644 --- a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx +++ b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx @@ -6,6 +6,7 @@ import {debounce, map, type Observable, of, tap, timer} from 'rxjs' import { type GeneralPreviewLayoutKey, useI18nText, + usePerspective, useReleases, useSchema, useTranslation, @@ -75,6 +76,7 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi const {childItemId, isActive, pane, paneKey, sortOrder: sortOrderRaw, layout} = props const schema = useSchema() const releases = useReleases() + const {bundlesPerspective} = usePerspective() const {displayOptions, options} = pane const {apiVersion, filter} = options const params = useShallowUnique(options.params || EMPTY_RECORD) @@ -111,7 +113,7 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi } = useDocumentList({ apiVersion, filter, - perspective: releases.stack, + perspective: bundlesPerspective, params, searchQuery: searchQuery?.trim(), sortOrder,