Skip to content

Commit

Permalink
feat(sanity): add release layering foundations
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Oct 24, 2024
1 parent 9dbac15 commit 2589a0e
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {styled} from 'styled-components'

import {useDateTimeFormat, useRelativeTime} from '../../hooks'
import {useTranslation} from '../../i18n'
import {type VersionsRecord} from '../../preview/utils/getPreviewStateObservable'
import {type BundleDocument} from '../../store/bundles'
import {PerspectiveBadge} from '../perspective/PerspectiveBadge'

Expand All @@ -13,6 +14,8 @@ interface DocumentStatusProps {
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
// eslint-disable-next-line
versions?: VersionsRecord
singleLine?: boolean
currentGlobalBundle?: Partial<BundleDocument>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import {Text} from '@sanity/ui'
import {useMemo} from 'react'
import {styled} from 'styled-components'

import {type VersionsRecord} from '../../preview/utils/getPreviewStateObservable'

interface DocumentStatusProps {
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
// eslint-disable-next-line
versions?: VersionsRecord
}

const Root = styled(Text)`
Expand Down
73 changes: 61 additions & 12 deletions packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts
Original file line number Diff line number Diff line change
@@ -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<string, PreparedSnapshot>

type VersionTuple = [bundleId: string, snapshot: PreparedSnapshot]

export interface PreviewState {
isLoading?: boolean
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
versions: VersionsRecord
}

const isLiveEditEnabled = (schemaType: SchemaType) => schemaType.liveEdit === true
Expand All @@ -25,31 +35,70 @@ export function getPreviewStateObservable(
schemaType: SchemaType,
documentId: string,
title: ReactNode,
perspective?: string,
perspective: {
bundleIds: string[]
bundleStack: string[]
} = {
bundleIds: [],
bundleStack: [],
},
): Observable<PreviewState> {
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<string, Observable<VersionTuple>>((bundleId) =>
documentPreviewStore
.observeForPreview({_id: getVersionId(documentId, bundleId)}, schemaType)
.pipe(map((storeValue) => [bundleId, storeValue])),
),
scan<VersionTuple, VersionsRecord>((byBundleId, [bundleId, value]) => {
if (value.snapshot === null) {
return omit({...byBundleId}, [bundleId])
}

return {
...byBundleId,
[bundleId]: value,
}
}, {}),
startWith<VersionsRecord>({}),
)

// 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<PreparedSnapshot>({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: {},
}),
)
}
10 changes: 7 additions & 3 deletions packages/sanity/src/core/store/_legacy/datastores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -299,26 +300,29 @@ 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<BundlesStore>({
dependencies: [workspace, addonDataset, currentUser],
dependencies: [workspace, addonDataset, currentUser, router.perspectiveState],
namespace: 'BundlesStore',
}) ||
createBundlesStore({
addonClient: addonDataset.client,
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])
}
5 changes: 4 additions & 1 deletion packages/sanity/src/core/store/bundles/createBundlesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const INITIAL_STATE: bundlesReducerState = {
bundles: new Map(),
deletedBundles: {},
state: 'initialising',
releaseStack: [],
}

const NOOP_BUNDLE_STORE: BundlesStore = {
Expand All @@ -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}),
Expand All @@ -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

Expand Down Expand Up @@ -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),
)
Expand Down
57 changes: 57 additions & 0 deletions packages/sanity/src/core/store/bundles/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {DRAFTS_FOLDER, resolveBundlePerspective} from 'sanity'

import {type BundleDocument} from './types'

interface BundleDeletedAction {
Expand Down Expand Up @@ -42,6 +44,12 @@ export interface bundlesReducerState {
deletedBundles: Record<string, BundleDocument>
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) {
Expand All @@ -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': {
Expand All @@ -71,6 +80,10 @@ export function bundlesReducer(
return {
...state,
bundles: bundlesById,
releaseStack: getReleaseStack({
bundles: bundlesById,
perspective,
}),
}
}

Expand All @@ -82,6 +95,10 @@ export function bundlesReducer(
return {
...state,
bundles: currentBundles,
releaseStack: getReleaseStack({
bundles: currentBundles,
perspective,
}),
}
}

Expand All @@ -105,6 +122,7 @@ export function bundlesReducer(
...state,
bundles: currentBundles,
deletedBundles: nextDeletedBundles,
releaseStack: [...state.releaseStack].filter((id) => id !== deletedBundleId),
}
}

Expand All @@ -117,10 +135,49 @@ export function bundlesReducer(
return {
...state,
bundles: currentBundles,
releaseStack: getReleaseStack({
bundles: currentBundles,
perspective,
}),
}
}

default:
return state
}
}

function getReleaseStack({
bundles,
perspective,
}: {
bundles?: Map<string, BundleDocument>
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
}
}
9 changes: 9 additions & 0 deletions packages/sanity/src/core/store/bundles/useBundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import {type BundleDocument} from './types'

interface BundlesState {
data: BundleDocument[] | null
bundles: Map<string, BundleDocument>
deletedBundles: Record<string, BundleDocument>
error?: Error
loading: boolean
dispatch: React.Dispatch<bundlesReducerAction>

/**
* An array of release ids ordered chronologically to represent the state of documents at the
* given point in time.
*/
stack: string[]
}

/**
Expand All @@ -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,
}
}
Loading

0 comments on commit 2589a0e

Please sign in to comment.