Skip to content

Commit

Permalink
feat(core): move bundlesPerspective to usePerspective hook, sort rele…
Browse files Browse the repository at this point in the history
…ases (#7682)
  • Loading branch information
pedrobonamin authored Oct 28, 2024
1 parent d4db504 commit 81c5fc3
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 143 deletions.
5 changes: 2 additions & 3 deletions packages/sanity/src/core/releases/components/ReleasesMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
232 changes: 232 additions & 0 deletions packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReleaseDocument, 'metadata'> & {
metadata: Partial<ReleaseDocument['metadata']>
}
>,
): 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)
},
)
})
25 changes: 21 additions & 4 deletions packages/sanity/src/core/releases/hooks/usePerspective.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,17 +23,21 @@ 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[]
}

/**
* TODO: Improve distinction between global and pane perspectives.
*
* @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) => {
Expand All @@ -44,7 +50,6 @@ export function usePerspective(selectedPerspective?: string): PerspectiveValue {
}

router.navigateStickyParam('perspective', perspectiveParam)
dispatch({type: 'PERSPECTIVE_SET', payload: perspectiveParam})
}

const selectedBundle =
Expand All @@ -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,
}
}
77 changes: 77 additions & 0 deletions packages/sanity/src/core/releases/hooks/utils.ts
Original file line number Diff line number Diff line change
@@ -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.<releaseName> 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))
}
Loading

0 comments on commit 81c5fc3

Please sign in to comment.