-
Notifications
You must be signed in to change notification settings - Fork 450
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sanity): add Rendering Context Store
- Loading branch information
Showing
9 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
packages/sanity/src/core/store/renderingContext/coreUiRenderingContext.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import {expect, it} from 'vitest' | ||
|
||
import {coreUiRenderingContext} from './coreUiRenderingContext' | ||
|
||
const coreUiProductionContext = { | ||
mode: 'core-ui', | ||
env: 'production', | ||
} | ||
|
||
const coreUiStagingContext = { | ||
mode: 'core-ui', | ||
env: 'staging', | ||
} | ||
|
||
const coreUiUnsupportedContext = { | ||
mode: 'evil-mode', | ||
env: 'production', | ||
} | ||
|
||
it('ignores the `_context` URL search parameter if the mode is not "core-ui"', () => { | ||
expect(() => coreUiRenderingContext(urlSearch(coreUiUnsupportedContext))).toMatchEmissions([ | ||
[undefined, undefined], | ||
]) | ||
}) | ||
|
||
it('parses the `_context` URL search parameter', () => { | ||
expect(() => coreUiRenderingContext(urlSearch(coreUiProductionContext))).toMatchEmissions([ | ||
[ | ||
undefined, | ||
{ | ||
name: 'coreUi', | ||
metadata: { | ||
environment: 'production', | ||
}, | ||
}, | ||
], | ||
]) | ||
|
||
expect(() => coreUiRenderingContext(urlSearch(coreUiStagingContext))).toMatchEmissions([ | ||
[ | ||
undefined, | ||
{ | ||
name: 'coreUi', | ||
metadata: { | ||
environment: 'staging', | ||
}, | ||
}, | ||
], | ||
]) | ||
}) | ||
|
||
it('fails gracefully if the `_context` URL search parameter cannot be parsed', () => { | ||
const invalidCoreUiContextString = urlSearch(coreUiProductionContext).slice(0, -10) | ||
|
||
expect(() => coreUiRenderingContext(invalidCoreUiContextString)).toMatchEmissions([ | ||
[undefined, undefined], | ||
]) | ||
}) | ||
|
||
function urlSearch(context: unknown): string { | ||
return new URLSearchParams({ | ||
_context: JSON.stringify(context), | ||
}).toString() | ||
} |
66 changes: 66 additions & 0 deletions
66
packages/sanity/src/core/store/renderingContext/coreUiRenderingContext.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { | ||
catchError, | ||
distinctUntilChanged, | ||
map, | ||
of, | ||
type OperatorFunction, | ||
pipe, | ||
switchMap, | ||
} from 'rxjs' | ||
|
||
import {isCoreUiRenderingContext, type StudioRenderingContext} from './types' | ||
|
||
// Core UI Rendering Context is provided via the URL query string, and remains static the entire | ||
// duration Studio is rendered inside the Core UI iframe. | ||
// | ||
// However, the URL query string is liable to be lost during Studio's lifecycle (for example, when | ||
// the user navigates to a different tool). Therefore, the URL query string is captured as soon as | ||
// this code is evaluated, and later referenced when a consumer subscribes to the store. | ||
const INITIAL_URL_SEARCH = location?.search | ||
|
||
const CORE_UI_MODE_NAME = 'core-ui' | ||
const CORE_UI_CONTEXT_SEARCH_PARAM = '_context' | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function coreUiRenderingContext( | ||
urlSearch: string = INITIAL_URL_SEARCH, | ||
): OperatorFunction<StudioRenderingContext | undefined, StudioRenderingContext | undefined> { | ||
return pipe( | ||
switchMap((renderingContext) => { | ||
if (renderingContext) { | ||
return of(renderingContext) | ||
} | ||
|
||
return of(urlSearch).pipe( | ||
distinctUntilChanged(), | ||
map((search) => new URLSearchParams(search).get(CORE_UI_CONTEXT_SEARCH_PARAM)), | ||
map((serializedContext) => { | ||
if (serializedContext === null) { | ||
return undefined | ||
} | ||
|
||
const {mode, env} = JSON.parse(serializedContext) | ||
|
||
const coreUirenderingContext = { | ||
name: mode === CORE_UI_MODE_NAME ? 'coreUi' : undefined, | ||
metadata: { | ||
environment: env, | ||
}, | ||
} | ||
|
||
if (isCoreUiRenderingContext(coreUirenderingContext)) { | ||
return coreUirenderingContext | ||
} | ||
|
||
return undefined | ||
}), | ||
catchError((error) => { | ||
console.warn('Error parsing rendering context:', error) | ||
return of(undefined) | ||
}), | ||
) | ||
}), | ||
) | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/sanity/src/core/store/renderingContext/createRenderingContextStore.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import {firstValueFrom} from 'rxjs' | ||
import {describe, expect, it} from 'vitest' | ||
|
||
import {createRenderingContextStore} from './createRenderingContextStore' | ||
|
||
describe('renderingContext', () => { | ||
it('emits rendering context', async () => { | ||
const {renderingContext} = createRenderingContextStore() | ||
|
||
expect(await firstValueFrom(renderingContext)).toEqual({ | ||
name: 'default', | ||
metadata: {}, | ||
}) | ||
}) | ||
}) | ||
|
||
describe('capabilities', () => { | ||
it('emits capabilities', async () => { | ||
const {capabilities} = createRenderingContextStore() | ||
expect(await firstValueFrom(capabilities)).toEqual({}) | ||
}) | ||
}) |
30 changes: 30 additions & 0 deletions
30
packages/sanity/src/core/store/renderingContext/createRenderingContextStore.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
packages/sanity/src/core/store/renderingContext/defaultRenderingContext.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import {expect, it} from 'vitest' | ||
|
||
import {defaultRenderingContext} from './defaultRenderingContext' | ||
|
||
it("emits the subject if it's not `undefined`", () => { | ||
expect(defaultRenderingContext).toMatchEmissions([ | ||
[ | ||
{ | ||
name: 'coreUi', | ||
metadata: { | ||
environment: 'production', | ||
}, | ||
}, | ||
{ | ||
name: 'coreUi', | ||
metadata: { | ||
environment: 'production', | ||
}, | ||
}, | ||
], | ||
]) | ||
}) | ||
|
||
it('emits the the default context if the subject is `undefined`', () => { | ||
expect(defaultRenderingContext).toMatchEmissions([ | ||
[ | ||
undefined, | ||
{ | ||
name: 'default', | ||
metadata: {}, | ||
}, | ||
], | ||
]) | ||
}) |
18 changes: 18 additions & 0 deletions
18
packages/sanity/src/core/store/renderingContext/defaultRenderingContext.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
18 changes: 18 additions & 0 deletions
18
packages/sanity/src/core/store/renderingContext/listCapabilities.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
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: string | ||
} | ||
> | ||
|
||
/** | ||
* @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' | ||
) | ||
} |