Skip to content

Commit

Permalink
feat: add shared state (#2120)
Browse files Browse the repository at this point in the history
* feat: add shared state

* refactor(presentation): improve preview header component types
  • Loading branch information
rdunk authored Nov 11, 2024
1 parent 1aa419f commit 1fb8143
Show file tree
Hide file tree
Showing 18 changed files with 473 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {useSharedState} from '@sanity/visual-editing'
import {FunctionComponent} from 'react'

export const OverlayHighlight: FunctionComponent = () => {
const overlayEnabled = useSharedState<boolean>('overlay-enabled')

if (!overlayEnabled) {
return null
}

return (
<div
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 255, 0.25)',
}}
/>
)
}
34 changes: 26 additions & 8 deletions apps/page-builder-demo/src/components/overlays/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
'use client'

import {OverlayComponentResolver} from '@sanity/visual-editing'
import {OverlayComponent, OverlayComponentResolver, useSharedState} from '@sanity/visual-editing'
import {
defineOverlayComponent,
UnionInsertMenuOverlay,
} from '@sanity/visual-editing/unstable_overlay-components'
import {ExcitingTitleControl} from './ExcitingTitleControl'
import {OverlayHighlight} from './OverlayHighlight'
import {ProductModelRotationControl} from './ProductModelRotationControl'

export const components: OverlayComponentResolver = (props) => {
const {type, node, parent} = props
const {element, type, node, parent} = props

const components: Array<
| OverlayComponent<Record<string, unknown>, any>
| {
component: OverlayComponent<Record<string, unknown>, any>
props?: Record<string, unknown>
}
> = [OverlayHighlight]

if (type === 'string' && node.path === 'title') {
return ExcitingTitleControl
components.push(ExcitingTitleControl)
}

if (type === 'object' && node.path.endsWith('rotations')) {
return defineOverlayComponent(ProductModelRotationControl)
components.push(ProductModelRotationControl)
}

if (parent?.type === 'union') {
return defineOverlayComponent(UnionInsertMenuOverlay, {
direction: 'vertical',
})
const parentDataset = element.parentElement?.dataset || {}

const direction = (parentDataset.direction ?? 'vertical') as 'vertical' | 'horizontal'

const hoverAreaExtent = parentDataset.hoverExtent || 48

components.push(
defineOverlayComponent(UnionInsertMenuOverlay, {
direction,
hoverAreaExtent,
}),
)
}

return undefined
return components
}
41 changes: 41 additions & 0 deletions apps/studio/presentation/CustomHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {CheckmarkIcon, CloseIcon, EllipsisVerticalIcon} from '@sanity/icons'
import {useSharedState} from '@sanity/presentation'
import type {PreviewHeaderProps} from '@sanity/presentation'
import {Button, Menu, MenuButton, MenuItem} from '@sanity/ui'
import {useState, type FunctionComponent} from 'react'

export const CustomHeader: FunctionComponent<PreviewHeaderProps> = (props) => {
const [enabled, setEnabled] = useState(false)

useSharedState('overlay-enabled', enabled)

return (
<>
{props.renderDefault(props)}
<MenuButton
button={
<Button fontSize={1} icon={EllipsisVerticalIcon} mode="bleed" padding={2} space={2} />
}
id="custom-menu"
menu={
<Menu style={{maxWidth: 240}}>
<MenuItem
fontSize={1}
icon={enabled ? CloseIcon : CheckmarkIcon}
onClick={() => setEnabled((enabled) => !enabled)}
padding={3}
tone={enabled ? 'caution' : 'positive'}
text={enabled ? 'Disable Highlighting' : 'Enable Highlighting'}
/>
</Menu>
}
popover={{
animate: true,
constrainSize: true,
placement: 'bottom',
portal: true,
}}
/>
</>
)
}
4 changes: 4 additions & 0 deletions apps/studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import {debugSecrets} from '@sanity/preview-url-secret/sanity-plugin-debug-secrets'
import {visionTool} from '@sanity/vision'
import {defineConfig, definePlugin, type PluginOptions} from 'sanity'
import {CustomHeader} from './presentation/CustomHeader'
import {CustomNavigator} from './presentation/CustomNavigator'
import {StegaDebugger} from './presentation/DebugStega'

Expand Down Expand Up @@ -138,6 +139,9 @@ export default defineConfig([
workspaces['page-builder-demo'].tool,
),
components: {
unstable_header: {
component: CustomHeader,
},
unstable_navigator: {
minWidth: 120,
maxWidth: 240,
Expand Down
110 changes: 57 additions & 53 deletions packages/presentation/src/PresentationTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from './constants'
import {useUnique, useWorkspace, type CommentIntentGetter} from './internals'
import {debounce} from './lib/debounce'
import {SharedStateProvider} from './overlays/SharedStateProvider'
import {Panel} from './panels/Panel'
import {Panels} from './panels/Panels'
import {PresentationContent} from './PresentationContent'
Expand Down Expand Up @@ -92,7 +93,7 @@ export default function PresentationTool(props: {
const components = tool.options?.components
const _previewUrl = tool.options?.previewUrl
const name = tool.name || DEFAULT_TOOL_NAME
const {unstable_navigator} = components || {}
const {unstable_navigator, unstable_header} = components || {}

const {navigate: routerNavigate, state: routerState} = useRouter() as RouterContextValue & {
state: PresentationStateParams
Expand Down Expand Up @@ -516,58 +517,61 @@ export default function PresentationTool(props: {
>
<PresentationNavigateProvider navigate={navigate}>
<PresentationParamsProvider params={params}>
<Container height="fill">
<Panels>
<PresentationNavigator />
<Panel
id="preview"
minWidth={325}
defaultSize={navigatorEnabled ? 50 : 75}
order={3}
>
<Flex direction="column" flex={1} height="fill" ref={setBoundaryElement}>
<BoundaryElementProvider element={boundaryElement}>
<Preview
canSharePreviewAccess={canSharePreviewAccess}
canToggleSharePreviewAccess={canToggleSharePreviewAccess}
canUseSharedPreviewAccess={canUseSharedPreviewAccess}
dispatch={dispatch}
iframe={state.iframe}
initialUrl={initialPreviewUrl}
loadersConnection={loadersConnection}
navigatorEnabled={navigatorEnabled}
onPathChange={handlePreviewPath}
onRefresh={handleRefresh}
openPopup={handleOpenPopup}
overlaysConnection={overlaysConnection}
previewUrl={params.preview}
perspective={perspective}
ref={iframeRef}
setPerspective={setPerspective}
setViewport={setViewport}
targetOrigin={targetOrigin}
toggleNavigator={toggleNavigator}
toggleOverlay={toggleOverlay}
viewport={viewport}
visualEditing={state.visualEditing}
/>
</BoundaryElementProvider>
</Flex>
</Panel>
<PresentationContent
documentId={params.id}
documentsOnPage={documentsOnPage}
documentType={params.type}
getCommentIntent={getCommentIntent}
mainDocumentState={mainDocumentState}
onFocusPath={handleFocusPath}
onStructureParams={handleStructureParams}
searchParams={searchParams}
setDisplayedDocument={setDisplayedDocument}
structureParams={structureParams}
/>
</Panels>
</Container>
<SharedStateProvider comlink={visualEditingComlink}>
<Container height="fill">
<Panels>
<PresentationNavigator />
<Panel
id="preview"
minWidth={325}
defaultSize={navigatorEnabled ? 50 : 75}
order={3}
>
<Flex direction="column" flex={1} height="fill" ref={setBoundaryElement}>
<BoundaryElementProvider element={boundaryElement}>
<Preview
canSharePreviewAccess={canSharePreviewAccess}
canToggleSharePreviewAccess={canToggleSharePreviewAccess}
canUseSharedPreviewAccess={canUseSharedPreviewAccess}
dispatch={dispatch}
header={unstable_header}
iframe={state.iframe}
initialUrl={initialPreviewUrl}
loadersConnection={loadersConnection}
navigatorEnabled={navigatorEnabled}
onPathChange={handlePreviewPath}
onRefresh={handleRefresh}
openPopup={handleOpenPopup}
overlaysConnection={overlaysConnection}
previewUrl={params.preview}
perspective={perspective}
ref={iframeRef}
setPerspective={setPerspective}
setViewport={setViewport}
targetOrigin={targetOrigin}
toggleNavigator={toggleNavigator}
toggleOverlay={toggleOverlay}
viewport={viewport}
visualEditing={state.visualEditing}
/>
</BoundaryElementProvider>
</Flex>
</Panel>
<PresentationContent
documentId={params.id}
documentsOnPage={documentsOnPage}
documentType={params.type}
getCommentIntent={getCommentIntent}
mainDocumentState={mainDocumentState}
onFocusPath={handleFocusPath}
onStructureParams={handleStructureParams}
searchParams={searchParams}
setDisplayedDocument={setDisplayedDocument}
structureParams={structureParams}
/>
</Panels>
</Container>
</SharedStateProvider>
</PresentationParamsProvider>
</PresentationNavigateProvider>
</PresentationProvider>
Expand Down
7 changes: 7 additions & 0 deletions packages/presentation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
usePresentationNavigate,
} from './usePresentationNavigate'
export {usePresentationParams} from './usePresentationParams'
export {useSharedState} from './overlays/useSharedState'
export type {PreviewHeaderProps} from './preview/PreviewHeader'
export type {PreviewProps} from './preview/Preview'
export {
Expand All @@ -42,3 +43,9 @@ export {
type PresentationState,
type VisualEditingOverlaysToggleAction,
} from './reducers/presentationReducer'
export type {
Serializable,
SerializableArray,
SerializableObject,
SerializablePrimitive,
} from '@repo/visual-editing-helpers'
9 changes: 9 additions & 0 deletions packages/presentation/src/overlays/SharedStateContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {Serializable} from '@repo/visual-editing-helpers'
import {createContext} from 'react'

export interface SharedStateContextValue {
removeValue: (key: string) => void
setValue: (key: string, value: Serializable) => void
}

export const SharedStateContext = createContext<SharedStateContextValue | null>(null)
50 changes: 50 additions & 0 deletions packages/presentation/src/overlays/SharedStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type {Serializable, SerializableObject} from '@repo/visual-editing-helpers'
import {
useCallback,
useEffect,
useMemo,
useRef,
type FunctionComponent,
type PropsWithChildren,
} from 'react'
import type {VisualEditingConnection} from '../types'
import {SharedStateContext, type SharedStateContextValue} from './SharedStateContext'

export const SharedStateProvider: FunctionComponent<
PropsWithChildren<{
comlink: VisualEditingConnection | null
}>
> = function (props) {
const {comlink, children} = props

const sharedState = useRef<SerializableObject>({})

useEffect(() => {
return comlink?.on('visual-editing/shared-state', () => {
return {state: sharedState.current}
})
}, [comlink])

const setValue = useCallback(
(key: string, value: Serializable) => {
sharedState.current[key] = value
comlink?.post({type: 'presentation/shared-state', data: {key, value}})
},
[comlink],
)

const removeValue = useCallback(
(key: string) => {
comlink?.post({type: 'presentation/shared-state', data: {key}})
delete sharedState.current[key]
},
[comlink],
)

const context = useMemo<SharedStateContextValue>(
() => ({removeValue, setValue}),
[removeValue, setValue],
)

return <SharedStateContext.Provider value={context}>{children}</SharedStateContext.Provider>
}
25 changes: 25 additions & 0 deletions packages/presentation/src/overlays/useSharedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {Serializable} from '@repo/visual-editing-helpers'
import {useContext, useEffect} from 'react'
import {SharedStateContext} from './SharedStateContext'

export const useSharedState = (key: string, value: Serializable): undefined => {
const context = useContext(SharedStateContext)

if (!context) {
throw new Error('Preview Snapshots context is missing')
}

const {removeValue, setValue} = context

useEffect(() => {
setValue(key, value)
}, [key, value, setValue])

useEffect(() => {
return () => {
removeValue(key)
}
}, [key, removeValue])

return undefined
}
Loading

0 comments on commit 1fb8143

Please sign in to comment.