Skip to content

Commit

Permalink
feat(sanity): add Rendering Context Store
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Feb 11, 2025
1 parent 8e1ed89 commit 2e92148
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/sanity/src/core/store/_legacy/datastores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../previ
import {useSource, useWorkspace} from '../../studio'
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../studioClient'
import {createKeyValueStore, type KeyValueStore} from '../key-value'
import {createRenderingContextStore} from '../renderingContext/createRenderingContextStore'
import {type RenderingContextStore} from '../renderingContext/types'
import {useCurrentUser} from '../user'
import {
type ConnectionStatusStore,
Expand Down Expand Up @@ -289,3 +291,24 @@ export function useKeyValueStore(): KeyValueStore {
return keyValueStore
}, [client, resourceCache, workspace])
}

/** @internal */
export function useRenderingContextStore(): RenderingContextStore {
const resourceCache = useResourceCache()

return useMemo(() => {
const renderingContextStore =
resourceCache.get<RenderingContextStore>({
dependencies: [],
namespace: 'RenderingContextStore',
}) || createRenderingContextStore()

resourceCache.set({
dependencies: [],
namespace: 'RenderingContextStore',
value: renderingContextStore,
})

return renderingContextStore
}, [resourceCache])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {distinctUntilChanged, map, of, type OperatorFunction, pipe, switchMap} from 'rxjs'

import {isCoreUiRenderingContext, type StudioRenderingContext} from './types'

// Core UI Rendering Context is provided via the URL hash, and remains static the entire duration
// Studio is rendered inside the Core UI iframe.
//
// However, the URL hash is liable to be lost when Studio renders (for example, when
// `ActiveWorkspaceMatcher` performs a redirect, or when the user navigates inside the app).
// Therefore, the URL hash is captured as soon as this code is evaluated, and later referenced
// when a consumer subscribes to the store.
const INITIAL_URL_HASH = location?.hash

const CORE_UI_MODE_PARAMETER = 'mode'
const CORE_UI_MODE_DELIMITER = '--'
const CORE_UI_MODE_NAME = 'core-ui'

/**
* @internal
*/
export function coreUiRenderingContext(): OperatorFunction<
StudioRenderingContext | undefined,
StudioRenderingContext | undefined
> {
return pipe(
switchMap((renderingContext) => {
if (renderingContext) {
return of(renderingContext)
}

return of(INITIAL_URL_HASH.slice(1)).pipe(
distinctUntilChanged(),
map((hash) => new URLSearchParams(hash).get(CORE_UI_MODE_PARAMETER)),
map((coreUiHashParam) => {
if (coreUiHashParam === null) {
return undefined
}

const [mode, environment] = coreUiHashParam.split(CORE_UI_MODE_DELIMITER)

const coreUirenderingContext = {
name: mode === CORE_UI_MODE_NAME ? 'coreUi' : undefined,
metadata: {
environment,
},
}

if (isCoreUiRenderingContext(coreUirenderingContext)) {
return coreUirenderingContext
}

return undefined
}),
)
}),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {of, shareReplay} from 'rxjs'

import {coreUiRenderingContext} from './coreUiRenderingContext'
import {defaultRenderingContext} from './defaultRenderingContext'
import {listCapabilities} from './listCapabilities'
import {type RenderingContextStore} from './types'

/**
* Rendering Context Store provides information about where Studio is being rendered, and which
* capabilities are provided by the rendering context.
*
* This can be used to adapt parts of the Studio UI that are provided by the rendering context,
* such as the global user menu.
*
* @internal
*/
export function createRenderingContextStore(): RenderingContextStore {
const renderingContext = of(undefined).pipe(
coreUiRenderingContext(),
defaultRenderingContext(),
shareReplay(1),
)

const capabilities = renderingContext.pipe(listCapabilities(), shareReplay(1))

return {
renderingContext,
capabilities,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {map, type OperatorFunction} from 'rxjs'

import {type DefaultRenderingContext, type StudioRenderingContext} from './types'

const DEFAULT_RENDERING_CONTEXT: DefaultRenderingContext = {
name: 'default',
metadata: {},
}

/**
* @internal
*/
export function defaultRenderingContext(): OperatorFunction<
StudioRenderingContext | undefined,
StudioRenderingContext
> {
return map((renderingContext) => renderingContext ?? DEFAULT_RENDERING_CONTEXT)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {map, type OperatorFunction} from 'rxjs'

import {type CapabilityRecord, type StudioRenderingContext} from './types'

const capabilitiesByRenderingContext: Record<StudioRenderingContext['name'], CapabilityRecord> = {
coreUi: {
globalUserMenu: true,
globalWorkspaceControl: true,
},
default: {},
}

/**
* @internal
*/
export function listCapabilities(): OperatorFunction<StudioRenderingContext, CapabilityRecord> {
return map((renderingContext) => capabilitiesByRenderingContext[renderingContext.name])
}
77 changes: 77 additions & 0 deletions packages/sanity/src/core/store/renderingContext/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {type Observable} from 'rxjs'

/**
* @internal
*/
export type BaseStudioRenderingContext<
Name extends string = string,
Metadata = Record<PropertyKey, never>,
> = {
name: Name
metadata: Metadata
}

/**
* @internal
*/
export type DefaultRenderingContext = BaseStudioRenderingContext<'default'>

/**
* @internal
*/
export type CoreUiRenderingContext = BaseStudioRenderingContext<
'coreUi',
{
environment: 'staging' | 'production'
}
>

/**
* @internal
*/
export type StudioRenderingContext = DefaultRenderingContext | CoreUiRenderingContext

/**
* @internal
*/
export const capabilities = ['globalUserMenu', 'globalWorkspaceControl'] as const

/**
* @internal
*/
export type Capability = (typeof capabilities)[number]

/**
* @internal
*/
export type CapabilityRecord = Partial<Record<Capability, boolean>>

/**
* @internal
*/
export type RenderingContextStore = {
renderingContext: Observable<StudioRenderingContext>
capabilities: Observable<CapabilityRecord>
}

/**
* Check whether the provided value satisfies the `CoreUiRenderingContext` type.
*
* @internal
*/
export function isCoreUiRenderingContext(
maybeCoreUiRenderingContext: unknown,
): maybeCoreUiRenderingContext is CoreUiRenderingContext {
return (
typeof maybeCoreUiRenderingContext === 'object' &&
maybeCoreUiRenderingContext !== null &&
'name' in maybeCoreUiRenderingContext &&
maybeCoreUiRenderingContext.name === 'coreUi' &&
'metadata' in maybeCoreUiRenderingContext &&
typeof maybeCoreUiRenderingContext.metadata === 'object' &&
maybeCoreUiRenderingContext.metadata !== null &&
'environment' in maybeCoreUiRenderingContext.metadata &&
typeof maybeCoreUiRenderingContext.metadata.environment === 'string' &&
['production', 'staging'].includes(maybeCoreUiRenderingContext.metadata.environment)
)
}

0 comments on commit 2e92148

Please sign in to comment.