sanity 3.83.1-canary.23 → 3.84.1-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/_chunks-cjs/PostMessageSchema.js.map +1 -1
- package/lib/_chunks-cjs/PostMessageTelemetry.js +1 -1
- package/lib/_chunks-cjs/PostMessageTelemetry.js.map +1 -1
- package/lib/_chunks-cjs/PresentationToolGrantsCheck.js +429 -1417
- package/lib/_chunks-cjs/PresentationToolGrantsCheck.js.map +1 -1
- package/lib/_chunks-cjs/presentation.js +70 -22
- package/lib/_chunks-cjs/presentation.js.map +1 -1
- package/lib/_chunks-cjs/resources6.js +0 -8
- package/lib/_chunks-cjs/resources6.js.map +1 -1
- package/lib/_chunks-cjs/version.js +1 -1
- package/lib/_chunks-es/PostMessageSchema.mjs.map +1 -1
- package/lib/_chunks-es/PostMessageTelemetry.mjs +1 -1
- package/lib/_chunks-es/PostMessageTelemetry.mjs.map +1 -1
- package/lib/_chunks-es/PresentationToolGrantsCheck.mjs +437 -1406
- package/lib/_chunks-es/PresentationToolGrantsCheck.mjs.map +1 -1
- package/lib/_chunks-es/presentation.mjs +73 -24
- package/lib/_chunks-es/presentation.mjs.map +1 -1
- package/lib/_chunks-es/resources6.mjs +0 -8
- package/lib/_chunks-es/resources6.mjs.map +1 -1
- package/lib/_chunks-es/version.mjs +1 -1
- package/lib/_singletons.d.mts +61 -2889
- package/lib/_singletons.d.ts +61 -2889
- package/lib/presentation.d.mts +57 -2887
- package/lib/presentation.d.ts +57 -2887
- package/lib/presentation.js +4 -0
- package/lib/presentation.js.map +1 -1
- package/lib/presentation.mjs +5 -1
- package/package.json +15 -16
- package/src/presentation/PostMessageTelemetry.tsx +1 -1
- package/src/presentation/PresentationTool.tsx +76 -85
- package/src/presentation/PresentationToolGrantsCheck.tsx +75 -39
- package/src/presentation/document/LocationsBanner.tsx +13 -52
- package/src/presentation/i18n/resources.ts +0 -10
- package/src/presentation/index.ts +13 -0
- package/src/presentation/{util → lib}/parse.ts +1 -2
- package/src/presentation/overlays/schema/PostMessageSchema.tsx +0 -1
- package/src/presentation/panels/usePanelsStorage.ts +1 -1
- package/src/presentation/preview/IFrame.tsx +35 -83
- package/src/presentation/preview/Preview.tsx +56 -172
- package/src/presentation/preview/PreviewHeader.tsx +10 -43
- package/src/presentation/preview/PreviewLocationInput.tsx +24 -48
- package/src/presentation/preview/SharePreviewMenu.tsx +2 -2
- package/src/presentation/reducers/presentationReducer.ts +134 -0
- package/src/presentation/types.ts +7 -139
- package/src/presentation/useMainDocument.ts +12 -4
- package/src/presentation/useParams.ts +3 -2
- package/src/presentation/usePreviewUrl.ts +133 -0
- package/src/presentation/actors/create-preview-secret.ts +0 -19
- package/src/presentation/actors/read-shared-secret.ts +0 -18
- package/src/presentation/actors/resolve-allow-patterns.ts +0 -55
- package/src/presentation/actors/resolve-initial-url.ts +0 -65
- package/src/presentation/actors/resolve-preview-mode-url.ts +0 -72
- package/src/presentation/actors/resolve-preview-mode.ts +0 -66
- package/src/presentation/actors/resolve-url-from-preview-search-param.ts +0 -29
- package/src/presentation/machines/presentation-machine.ts +0 -101
- package/src/presentation/machines/preview-url.ts +0 -568
- package/src/presentation/useAllowPatterns.ts +0 -12
- package/src/presentation/useId.ts +0 -7
- package/src/presentation/usePresentationPerspective.ts +0 -14
- package/src/presentation/usePreviewUrlActorRef.ts +0 -96
- package/src/presentation/useReportInvalidPreviewSearchParam.tsx +0 -43
- package/src/presentation/useTargetOrigin.ts +0 -11
- /package/src/presentation/{util → lib}/debounce.ts +0 -0
@@ -1,15 +1,13 @@
|
|
1
|
-
/* eslint-disable no-nested-ternary */
|
2
1
|
import {DesktopIcon, MobileDeviceIcon, PanelLeftIcon, RefreshIcon} from '@sanity/icons'
|
3
2
|
import {withoutSecretSearchParams} from '@sanity/preview-url-secret/without-secret-search-params'
|
4
3
|
import {Box, Card, Flex, Hotkeys, Switch, Text} from '@sanity/ui'
|
5
|
-
import {useSelector} from '@xstate/react'
|
6
4
|
import {type RefObject, useCallback, useMemo} from 'react'
|
7
5
|
import {useTranslation} from 'sanity'
|
8
6
|
|
9
7
|
import {Button, Tooltip} from '../../ui-components'
|
10
8
|
import {presentationLocaleNamespace} from '../i18n'
|
9
|
+
import {ACTION_IFRAME_RELOAD} from '../reducers/presentationReducer'
|
11
10
|
import {type HeaderOptions} from '../types'
|
12
|
-
import {useId} from '../useId'
|
13
11
|
import {OpenPreviewButton} from './OpenPreviewButton'
|
14
12
|
import {type PreviewProps} from './Preview'
|
15
13
|
import {PreviewLocationInput} from './PreviewLocationInput'
|
@@ -26,6 +24,8 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
26
24
|
canSharePreviewAccess,
|
27
25
|
canToggleSharePreviewAccess,
|
28
26
|
canUseSharedPreviewAccess,
|
27
|
+
dispatch,
|
28
|
+
iframe,
|
29
29
|
iframeRef,
|
30
30
|
initialUrl,
|
31
31
|
navigatorEnabled,
|
@@ -33,7 +33,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
33
33
|
onRefresh,
|
34
34
|
openPopup,
|
35
35
|
overlaysConnection,
|
36
|
-
presentationRef,
|
37
36
|
perspective,
|
38
37
|
previewUrl,
|
39
38
|
setViewport,
|
@@ -41,7 +40,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
41
40
|
toggleNavigator,
|
42
41
|
toggleOverlay,
|
43
42
|
viewport,
|
44
|
-
|
43
|
+
visualEditing: {overlaysEnabled},
|
45
44
|
} = props
|
46
45
|
|
47
46
|
const {t} = useTranslation(presentationLocaleNamespace)
|
@@ -60,7 +59,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
60
59
|
if (!iframeRef.current) {
|
61
60
|
return
|
62
61
|
}
|
63
|
-
|
62
|
+
dispatch({type: ACTION_IFRAME_RELOAD})
|
64
63
|
// Funky way to reload an iframe without CORS issues
|
65
64
|
// eslint-disable-next-line no-self-assign
|
66
65
|
// ref.current.src = ref.current.src
|
@@ -68,17 +67,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
68
67
|
})
|
69
68
|
}
|
70
69
|
|
71
|
-
const isLoading = useSelector(presentationRef, (state) => state.matches('loading'))
|
72
|
-
const isLoaded = useSelector(presentationRef, (state) => state.matches('loaded'))
|
73
|
-
const isRefreshing = useSelector(presentationRef, (state) =>
|
74
|
-
state.matches({loaded: 'refreshing'}),
|
75
|
-
)
|
76
|
-
const isReloading = useSelector(presentationRef, (state) => state.matches({loaded: 'reloading'}))
|
77
|
-
const overlaysEnabled = useSelector(
|
78
|
-
presentationRef,
|
79
|
-
(state) => state.context.visualEditingOverlaysEnabled,
|
80
|
-
)
|
81
|
-
|
82
70
|
const previewLocationRoute = useMemo(() => {
|
83
71
|
const previewURL = new URL(previewUrl || '/', targetOrigin)
|
84
72
|
const {pathname, search} = withoutSecretSearchParams(previewURL)
|
@@ -86,14 +74,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
86
74
|
return `${pathname}${search}`
|
87
75
|
}, [previewUrl, targetOrigin])
|
88
76
|
|
89
|
-
const perspectiveToggleTooltipId = useId()
|
90
|
-
|
91
|
-
/**
|
92
|
-
* If the preview URL machine is busy it means it's about to change the target origin and reload the iframe,
|
93
|
-
* so we show a spinner
|
94
|
-
*/
|
95
|
-
const previewUrlBusy = useSelector(previewUrlRef, (state) => state.hasTag('busy'))
|
96
|
-
|
97
77
|
return (
|
98
78
|
<Flex align="center" gap={1} paddingX={1} style={{width: '100%'}}>
|
99
79
|
{toggleNavigator && (
|
@@ -146,10 +126,9 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
146
126
|
<Flex align="center" gap={3}>
|
147
127
|
<div style={{margin: -4}}>
|
148
128
|
<Switch
|
149
|
-
indeterminate={!isLoaded}
|
150
129
|
checked={overlaysEnabled}
|
151
130
|
onChange={toggleOverlay}
|
152
|
-
disabled={
|
131
|
+
disabled={iframe.status === 'loading' || overlaysConnection !== 'connected'}
|
153
132
|
/>
|
154
133
|
</div>
|
155
134
|
<Box>
|
@@ -163,24 +142,15 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
163
142
|
|
164
143
|
<Box flex={1}>
|
165
144
|
<PreviewLocationInput
|
166
|
-
previewUrlRef={previewUrlRef}
|
167
145
|
prefix={
|
168
146
|
<Box padding={1}>
|
169
147
|
<Tooltip
|
170
148
|
animate
|
171
149
|
content={
|
172
150
|
<Text size={1}>
|
173
|
-
{
|
151
|
+
{iframe.status === 'loaded'
|
174
152
|
? t('preview-frame.refresh-button.tooltip')
|
175
|
-
: t('preview-frame.status', {
|
176
|
-
context: isLoading
|
177
|
-
? 'loading'
|
178
|
-
: isRefreshing
|
179
|
-
? 'refreshing'
|
180
|
-
: isReloading
|
181
|
-
? 'reloading'
|
182
|
-
: 'unknown',
|
183
|
-
})}
|
153
|
+
: t('preview-frame.status', {context: iframe.status})}
|
184
154
|
</Text>
|
185
155
|
}
|
186
156
|
fallbackPlacements={['bottom-end']}
|
@@ -191,7 +161,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
191
161
|
aria-label={t('preview-frame.refresh-button.aria-label')}
|
192
162
|
icon={RefreshIcon}
|
193
163
|
mode="bleed"
|
194
|
-
loading={
|
164
|
+
loading={iframe.status === 'reloading' || iframe.status === 'refreshing'}
|
195
165
|
onClick={handleRefresh}
|
196
166
|
tooltipProps={null}
|
197
167
|
/>
|
@@ -199,6 +169,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
199
169
|
</Box>
|
200
170
|
}
|
201
171
|
onChange={onPathChange}
|
172
|
+
origin={previewLocationOrigin}
|
202
173
|
suffix={
|
203
174
|
<Box padding={1}>
|
204
175
|
<OpenPreviewButton
|
@@ -217,10 +188,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
|
|
217
188
|
<Flex align="center" flex="none" gap={1}>
|
218
189
|
<Tooltip
|
219
190
|
animate
|
220
|
-
// eslint-disable-next-line react/jsx-no-bind
|
221
|
-
ref={(node) => {
|
222
|
-
node?.style.setProperty('view-transition-name', perspectiveToggleTooltipId)
|
223
|
-
}}
|
224
191
|
content={
|
225
192
|
<Text size={1}>
|
226
193
|
{t('preview-frame.viewport-button.tooltip', {
|
@@ -2,6 +2,7 @@ import {ResetIcon} from '@sanity/icons'
|
|
2
2
|
import {TextInput, type TextInputClearButtonProps} from '@sanity/ui'
|
3
3
|
import {
|
4
4
|
type ChangeEvent,
|
5
|
+
type FunctionComponent,
|
5
6
|
type KeyboardEvent,
|
6
7
|
type ReactNode,
|
7
8
|
useCallback,
|
@@ -13,22 +14,17 @@ import {
|
|
13
14
|
import {useActiveWorkspace, useTranslation} from 'sanity'
|
14
15
|
|
15
16
|
import {presentationLocaleNamespace} from '../i18n'
|
16
|
-
import {type PreviewUrlRef} from '../machines/preview-url'
|
17
|
-
import {useAllowPatterns} from '../useAllowPatterns'
|
18
|
-
import {useTargetOrigin} from '../useTargetOrigin'
|
19
17
|
|
20
|
-
export
|
18
|
+
export const PreviewLocationInput: FunctionComponent<{
|
21
19
|
fontSize?: number
|
22
20
|
onChange: (value: string) => void
|
23
|
-
|
21
|
+
origin: string
|
24
22
|
padding?: number
|
25
23
|
prefix?: ReactNode
|
26
24
|
suffix?: ReactNode
|
27
25
|
value: string
|
28
|
-
})
|
29
|
-
const {fontSize = 1, onChange, padding = 3, prefix, suffix, value
|
30
|
-
const allowOrigins = useAllowPatterns(previewUrlRef)
|
31
|
-
const targetOrigin = useTargetOrigin(previewUrlRef)
|
26
|
+
}> = function (props) {
|
27
|
+
const {fontSize = 1, onChange, origin, padding = 3, prefix, suffix, value} = props
|
32
28
|
|
33
29
|
const {t} = useTranslation(presentationLocaleNamespace)
|
34
30
|
const {basePath = '/'} = useActiveWorkspace()?.activeWorkspace || {}
|
@@ -48,41 +44,29 @@ export function PreviewLocationInput(props: {
|
|
48
44
|
return
|
49
45
|
}
|
50
46
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
// ignore
|
56
|
-
}
|
47
|
+
const absoluteValue =
|
48
|
+
sessionValue.startsWith('/') || sessionValue === ''
|
49
|
+
? `${origin}${sessionValue}`
|
50
|
+
: sessionValue
|
57
51
|
|
58
|
-
if (
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}),
|
65
|
-
)
|
66
|
-
event.currentTarget.reportValidity()
|
67
|
-
return
|
68
|
-
}
|
69
|
-
// `origin` is an empty string '' if the Studio is embedded, and that's when we need to protect against recursion
|
70
|
-
} else if (
|
71
|
-
!targetOrigin &&
|
72
|
-
(absoluteValue.startsWith(`${basePath}/`) || absoluteValue === basePath)
|
73
|
-
) {
|
52
|
+
if (!absoluteValue.startsWith(`${origin}/`) && absoluteValue !== origin) {
|
53
|
+
setCustomValidity(t('preview-location-input.error', {origin, context: 'missing-origin'}))
|
54
|
+
return
|
55
|
+
}
|
56
|
+
// `origin` is an empty string '' if the Studio is embedded, and that's when we need to protect against recursion
|
57
|
+
if (!origin && (absoluteValue.startsWith(`${basePath}/`) || absoluteValue === basePath)) {
|
74
58
|
setCustomValidity(
|
75
59
|
t('preview-location-input.error', {basePath, context: 'same-base-path'}),
|
76
60
|
)
|
77
61
|
return
|
78
62
|
}
|
79
63
|
|
80
|
-
const nextValue = absoluteValue ===
|
64
|
+
const nextValue = absoluteValue === origin ? `${origin}/` : absoluteValue
|
81
65
|
|
82
66
|
setCustomValidity(undefined)
|
83
67
|
setSessionValue(undefined)
|
84
68
|
|
85
|
-
onChange(nextValue)
|
69
|
+
onChange(nextValue.slice(origin.length))
|
86
70
|
|
87
71
|
inputRef.current?.blur()
|
88
72
|
}
|
@@ -92,7 +76,7 @@ export function PreviewLocationInput(props: {
|
|
92
76
|
setSessionValue(undefined)
|
93
77
|
}
|
94
78
|
},
|
95
|
-
[
|
79
|
+
[basePath, onChange, origin, sessionValue, t],
|
96
80
|
)
|
97
81
|
|
98
82
|
const handleBlur = useCallback(() => {
|
@@ -100,21 +84,10 @@ export function PreviewLocationInput(props: {
|
|
100
84
|
setSessionValue(undefined)
|
101
85
|
}, [])
|
102
86
|
|
103
|
-
const handleClear = useCallback(() => {
|
104
|
-
setCustomValidity(undefined)
|
105
|
-
let nextValue = value
|
106
|
-
try {
|
107
|
-
nextValue = new URL(value, targetOrigin).toString()
|
108
|
-
} catch {
|
109
|
-
// ignore
|
110
|
-
}
|
111
|
-
setSessionValue(nextValue)
|
112
|
-
}, [targetOrigin, value])
|
113
|
-
|
114
87
|
useEffect(() => {
|
115
88
|
setCustomValidity(undefined)
|
116
89
|
setSessionValue(undefined)
|
117
|
-
}, [
|
90
|
+
}, [origin, value])
|
118
91
|
|
119
92
|
const resetButton: TextInputClearButtonProps = useMemo(() => ({icon: ResetIcon}), [])
|
120
93
|
|
@@ -125,7 +98,10 @@ export function PreviewLocationInput(props: {
|
|
125
98
|
customValidity={customValidity}
|
126
99
|
fontSize={fontSize}
|
127
100
|
onBlur={handleBlur}
|
128
|
-
onClear={
|
101
|
+
onClear={() => {
|
102
|
+
setCustomValidity(undefined)
|
103
|
+
setSessionValue(origin + value)
|
104
|
+
}}
|
129
105
|
onChange={handleChange}
|
130
106
|
onKeyDownCapture={handleKeyDown}
|
131
107
|
padding={padding}
|
@@ -135,7 +111,7 @@ export function PreviewLocationInput(props: {
|
|
135
111
|
ref={inputRef}
|
136
112
|
space={padding}
|
137
113
|
suffix={suffix}
|
138
|
-
value={sessionValue === undefined ?
|
114
|
+
value={sessionValue === undefined ? `${origin}${value}` : sessionValue}
|
139
115
|
/>
|
140
116
|
</>
|
141
117
|
)
|
@@ -108,7 +108,7 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent(
|
|
108
108
|
setDisabling(true)
|
109
109
|
await disablePreviewAccessSharing(
|
110
110
|
client,
|
111
|
-
'sanity/presentation',
|
111
|
+
'@sanity/presentation',
|
112
112
|
typeof window === 'undefined' ? '' : location.href,
|
113
113
|
currentUser?.id,
|
114
114
|
)
|
@@ -125,7 +125,7 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent(
|
|
125
125
|
|
126
126
|
const previewUrlSecret = await enablePreviewAccessSharing(
|
127
127
|
client,
|
128
|
-
'sanity/presentation',
|
128
|
+
'@sanity/presentation',
|
129
129
|
typeof window === 'undefined' ? '' : location.href,
|
130
130
|
currentUser?.id,
|
131
131
|
)
|
@@ -0,0 +1,134 @@
|
|
1
|
+
import {type Dispatch, type Reducer} from 'react'
|
2
|
+
import {boolean, fallback, object, parse, picklist} from 'valibot'
|
3
|
+
|
4
|
+
/** @public */
|
5
|
+
export interface PresentationState {
|
6
|
+
mainDocument: boolean
|
7
|
+
iframe: {
|
8
|
+
status: 'loading' | 'loaded' | 'refreshing' | 'reloading'
|
9
|
+
}
|
10
|
+
visualEditing: {
|
11
|
+
overlaysEnabled: boolean
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
/** @public */
|
16
|
+
export const ACTION_IFRAME_LOADED = 'ACTION_IFRAME_LOADED'
|
17
|
+
/** @public */
|
18
|
+
export const ACTION_IFRAME_REFRESH = 'ACTION_IFRAME_REFRESH'
|
19
|
+
/** @public */
|
20
|
+
export const ACTION_IFRAME_RELOAD = 'ACTION_IFRAME_RELOAD'
|
21
|
+
/** @public */
|
22
|
+
export const ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE = 'ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE'
|
23
|
+
|
24
|
+
/** @public */
|
25
|
+
export interface IframeLoadedAction {
|
26
|
+
type: typeof ACTION_IFRAME_LOADED
|
27
|
+
}
|
28
|
+
/** @public */
|
29
|
+
export interface IframeRefreshAction {
|
30
|
+
type: typeof ACTION_IFRAME_REFRESH
|
31
|
+
}
|
32
|
+
/** @public */
|
33
|
+
export interface IframeReloadAction {
|
34
|
+
type: typeof ACTION_IFRAME_RELOAD
|
35
|
+
}
|
36
|
+
/** @public */
|
37
|
+
export interface VisualEditingOverlaysToggleAction {
|
38
|
+
type: typeof ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE
|
39
|
+
enabled: boolean
|
40
|
+
}
|
41
|
+
|
42
|
+
/** @public */
|
43
|
+
export type PresentationAction =
|
44
|
+
| IframeLoadedAction
|
45
|
+
| IframeRefreshAction
|
46
|
+
| IframeReloadAction
|
47
|
+
| VisualEditingOverlaysToggleAction
|
48
|
+
|
49
|
+
export const presentationReducer: Reducer<
|
50
|
+
Readonly<PresentationState>,
|
51
|
+
Readonly<PresentationAction>
|
52
|
+
> = (state, action) => {
|
53
|
+
switch (action.type) {
|
54
|
+
case ACTION_IFRAME_LOADED:
|
55
|
+
return state.iframe.status === 'loaded'
|
56
|
+
? state
|
57
|
+
: {
|
58
|
+
...state,
|
59
|
+
iframe: {
|
60
|
+
...state.iframe,
|
61
|
+
status: 'loaded',
|
62
|
+
},
|
63
|
+
}
|
64
|
+
case ACTION_IFRAME_REFRESH:
|
65
|
+
return state.iframe.status === 'refreshing'
|
66
|
+
? state
|
67
|
+
: {
|
68
|
+
...state,
|
69
|
+
iframe: {
|
70
|
+
...state.iframe,
|
71
|
+
status: 'refreshing',
|
72
|
+
},
|
73
|
+
}
|
74
|
+
case ACTION_IFRAME_RELOAD:
|
75
|
+
return state.iframe.status === 'reloading'
|
76
|
+
? state
|
77
|
+
: {
|
78
|
+
...state,
|
79
|
+
iframe: {
|
80
|
+
...state.iframe,
|
81
|
+
status: 'reloading',
|
82
|
+
},
|
83
|
+
}
|
84
|
+
case ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE:
|
85
|
+
return toggleVisualEditingOverlays(state, action)
|
86
|
+
default:
|
87
|
+
return state
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
const toggleVisualEditingOverlays: Reducer<
|
92
|
+
Readonly<PresentationState>,
|
93
|
+
Readonly<VisualEditingOverlaysToggleAction>
|
94
|
+
> = (state, action) => {
|
95
|
+
if (state.visualEditing.overlaysEnabled === action.enabled) return state
|
96
|
+
return {
|
97
|
+
...state,
|
98
|
+
visualEditing: {
|
99
|
+
...state.visualEditing,
|
100
|
+
overlaysEnabled: action.enabled,
|
101
|
+
},
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
const mainDocumentSchema = fallback(boolean(), false)
|
106
|
+
|
107
|
+
const iframeStatusSchema = picklist(['loading', 'loaded', 'refreshing', 'reloading'])
|
108
|
+
|
109
|
+
const initStateSchema = object({
|
110
|
+
mainDocument: mainDocumentSchema,
|
111
|
+
iframe: object({
|
112
|
+
status: iframeStatusSchema,
|
113
|
+
}),
|
114
|
+
visualEditing: object({overlaysEnabled: boolean()}),
|
115
|
+
})
|
116
|
+
|
117
|
+
const INITIAL_PRESENTATION_STATE = {
|
118
|
+
mainDocument: false,
|
119
|
+
iframe: {
|
120
|
+
status: 'loading',
|
121
|
+
},
|
122
|
+
visualEditing: {
|
123
|
+
overlaysEnabled: false,
|
124
|
+
},
|
125
|
+
} as const satisfies PresentationState
|
126
|
+
|
127
|
+
export function presentationReducerInit(
|
128
|
+
state: Readonly<Partial<PresentationState>>,
|
129
|
+
): Readonly<PresentationState> {
|
130
|
+
return parse(initStateSchema, {...INITIAL_PRESENTATION_STATE, ...state})
|
131
|
+
}
|
132
|
+
|
133
|
+
/** @public */
|
134
|
+
export type DispatchPresentationAction = Dispatch<Readonly<PresentationAction>>
|
@@ -6,14 +6,17 @@ import {
|
|
6
6
|
type VisualEditingControllerMsg,
|
7
7
|
type VisualEditingNodeMsg,
|
8
8
|
} from '@sanity/presentation-comlink'
|
9
|
-
import {
|
9
|
+
import {
|
10
|
+
type PreviewUrlResolver,
|
11
|
+
type PreviewUrlResolverOptions,
|
12
|
+
} from '@sanity/preview-url-secret/define-preview-url'
|
10
13
|
import {type ComponentType} from 'react'
|
11
14
|
import {type Observable} from 'rxjs'
|
12
15
|
import {type DocumentStore, type SanityClient} from 'sanity'
|
13
16
|
|
14
17
|
import {type PreviewHeaderProps} from './preview/PreviewHeader'
|
15
18
|
|
16
|
-
export type {PreviewUrlResolver}
|
19
|
+
export type {PreviewUrlResolver, PreviewUrlResolverOptions}
|
17
20
|
|
18
21
|
/**
|
19
22
|
* Represents a document location
|
@@ -79,141 +82,7 @@ export interface HeaderOptions {
|
|
79
82
|
}
|
80
83
|
|
81
84
|
/** @public */
|
82
|
-
export
|
83
|
-
client: SanityClient
|
84
|
-
/**
|
85
|
-
* Equivalent to `location.origin`
|
86
|
-
*/
|
87
|
-
origin: string
|
88
|
-
/**
|
89
|
-
* The initial URL of the preview
|
90
|
-
*/
|
91
|
-
initialUrl: URL
|
92
|
-
}
|
93
|
-
|
94
|
-
/** @public */
|
95
|
-
export interface PreviewUrlInitialOptionContext {
|
96
|
-
client: SanityClient
|
97
|
-
/**
|
98
|
-
* Equivalent to `location.origin`
|
99
|
-
*/
|
100
|
-
origin: string
|
101
|
-
}
|
102
|
-
|
103
|
-
/** @public */
|
104
|
-
export interface PreviewUrlPreviewModeOptionContext {
|
105
|
-
client: SanityClient
|
106
|
-
/**
|
107
|
-
* Equivalent to `location.origin`
|
108
|
-
*/
|
109
|
-
origin: string
|
110
|
-
/**
|
111
|
-
* The origin on the URL that will be used in the preview iframe
|
112
|
-
*/
|
113
|
-
targetOrigin: string
|
114
|
-
}
|
115
|
-
|
116
|
-
/** @public */
|
117
|
-
export type PreviewUrlAllowOption =
|
118
|
-
| string
|
119
|
-
| string[]
|
120
|
-
| ((context: PreviewUrlAllowOptionContext) => string | string[] | Promise<string | string[]>)
|
121
|
-
|
122
|
-
/** @public */
|
123
|
-
export type PreviewUrlInitialOption =
|
124
|
-
| string
|
125
|
-
| ((context: PreviewUrlInitialOptionContext) => string | Promise<string>)
|
126
|
-
|
127
|
-
/** @public */
|
128
|
-
export type PreviewUrlPreviewModeOption =
|
129
|
-
| PreviewUrlPreviewMode
|
130
|
-
| ((
|
131
|
-
context: PreviewUrlPreviewModeOptionContext,
|
132
|
-
) => false | PreviewUrlPreviewMode | Promise<false | PreviewUrlPreviewMode>)
|
133
|
-
|
134
|
-
/** @public */
|
135
|
-
export interface PreviewUrlPreviewMode {
|
136
|
-
/**
|
137
|
-
* The route that enables Preview Mode
|
138
|
-
* @example '/api/preview'
|
139
|
-
* @example '/api/draft-mode/enable'
|
140
|
-
*/
|
141
|
-
enable: string
|
142
|
-
/**
|
143
|
-
* Allow sharing access to a preview with others.
|
144
|
-
* This is enabled/disabled in the Presentation Tool. It's initially disabled, and can be enabled by someone who has access to creating draft documents in the Studio.
|
145
|
-
* Custom roles can limit access to `_id in path("drafts.**") && _type == "sanity.previewUrlSecret"`.
|
146
|
-
* This will create a secret that is valid until sharing is disabled. Turning sharing off and on again will create a new secret and can be used to remove access for folks that got the link in an email but should no longer have access.
|
147
|
-
* Share URLs to previews will append this secret and give access to anyone who is given the URL, they don't need to be logged into the Studio or to Vercel.
|
148
|
-
*/
|
149
|
-
shareAccess?: boolean
|
150
|
-
/**
|
151
|
-
* The route that reports if Preview Mode is enabled or not, useful for debugging
|
152
|
-
* @example '/api/check-preview'
|
153
|
-
* @deprecated - this API is not yet implemented
|
154
|
-
*/
|
155
|
-
check?: string
|
156
|
-
/**
|
157
|
-
* The route that disables Preview Mode, useful for debugging
|
158
|
-
* @example '/api/disable-preview'
|
159
|
-
* @deprecated - this API is not yet implemented
|
160
|
-
*/
|
161
|
-
disable?: string
|
162
|
-
}
|
163
|
-
|
164
|
-
/**
|
165
|
-
* @public
|
166
|
-
*/
|
167
|
-
export interface PreviewUrlResolverOptions {
|
168
|
-
/**
|
169
|
-
* The default preview URL, used when the URL to use is not yet known, or there's no `&preview=...` search param in the studio URL.
|
170
|
-
* @example '/en/preview?q=shoes'
|
171
|
-
* @example 'https://example.com'
|
172
|
-
* @defaultValue `location.origin`
|
173
|
-
*/
|
174
|
-
initial?: PreviewUrlInitialOption
|
175
|
-
previewMode?: PreviewUrlPreviewModeOption
|
176
|
-
/**
|
177
|
-
* @defaultValue `location.origin`
|
178
|
-
* @deprecated - use `previewMode.initial` instead
|
179
|
-
*/
|
180
|
-
origin?: string
|
181
|
-
/**
|
182
|
-
* @defaultValue '/'
|
183
|
-
* @deprecated - use `previewMode.initial` instead
|
184
|
-
*/
|
185
|
-
preview?: string
|
186
|
-
/**
|
187
|
-
* @deprecated - use `previewMode` instead
|
188
|
-
*/
|
189
|
-
draftMode?: {
|
190
|
-
/**
|
191
|
-
* @deprecated - use `previewMode.enable` instead
|
192
|
-
*/
|
193
|
-
enable: string
|
194
|
-
/**
|
195
|
-
* @deprecated - use `previewMode.shareAccess` instead
|
196
|
-
*/
|
197
|
-
shareAccess?: boolean
|
198
|
-
/**
|
199
|
-
* @deprecated - use `previewMode.check` instead
|
200
|
-
*/
|
201
|
-
check?: string
|
202
|
-
/**
|
203
|
-
* @deprecated - use `previewMode.disable` instead
|
204
|
-
*/
|
205
|
-
disable?: string
|
206
|
-
}
|
207
|
-
}
|
208
|
-
|
209
|
-
/**
|
210
|
-
* @deprecated the `previewUrl.initial`, `previewUrl.allowOrigins` and `previewUrl.previewMode.enable` supports async functions that offer advanced control over how preview URLs are resolved
|
211
|
-
* @public
|
212
|
-
*/
|
213
|
-
export type DeprecatedPreviewUrlResolver = PreviewUrlResolver<SanityClient>
|
214
|
-
|
215
|
-
/** @public */
|
216
|
-
export type PreviewUrlOption = string | DeprecatedPreviewUrlResolver | PreviewUrlResolverOptions
|
85
|
+
export type PreviewUrlOption = string | PreviewUrlResolver<SanityClient> | PreviewUrlResolverOptions
|
217
86
|
|
218
87
|
/**
|
219
88
|
* Object of document location resolver definitions per document type
|
@@ -291,8 +160,6 @@ export interface PresentationPluginOptions {
|
|
291
160
|
icon?: ComponentType
|
292
161
|
name?: string
|
293
162
|
title?: string
|
294
|
-
allowOrigins?: PreviewUrlAllowOption
|
295
|
-
previewUrl: PreviewUrlOption
|
296
163
|
/**
|
297
164
|
* @deprecated use `resolve.locations` instead
|
298
165
|
*/
|
@@ -301,6 +168,7 @@ export interface PresentationPluginOptions {
|
|
301
168
|
mainDocuments?: DocumentResolver[]
|
302
169
|
locations?: DocumentLocationResolvers | DocumentLocationResolver
|
303
170
|
}
|
171
|
+
previewUrl: PreviewUrlOption
|
304
172
|
components?: {
|
305
173
|
unstable_header?: HeaderOptions
|
306
174
|
unstable_navigator?: NavigatorOptions
|
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
type MainDocument,
|
13
13
|
type MainDocumentState,
|
14
14
|
type PresentationNavigate,
|
15
|
+
type PreviewUrlOption,
|
15
16
|
} from './types'
|
16
17
|
|
17
18
|
// Helper function to "unwrap" a result when it is either explicitly provided or
|
@@ -96,10 +97,10 @@ export function useMainDocument(props: {
|
|
96
97
|
navigate?: PresentationNavigate
|
97
98
|
navigationHistory: RouterState[]
|
98
99
|
path?: string
|
99
|
-
|
100
|
+
previewUrl?: PreviewUrlOption
|
100
101
|
resolvers?: DocumentResolver[]
|
101
102
|
}): MainDocumentState | undefined {
|
102
|
-
const {navigate, navigationHistory, path,
|
103
|
+
const {navigate, navigationHistory, path, previewUrl, resolvers = []} = props
|
103
104
|
const {state: routerState} = useRouter()
|
104
105
|
const {perspectiveStack} = usePerspective()
|
105
106
|
const client = useClient({apiVersion: API_VERSION})
|
@@ -135,7 +136,14 @@ export function useMainDocument(props: {
|
|
135
136
|
})
|
136
137
|
|
137
138
|
useEffect(() => {
|
138
|
-
const
|
139
|
+
const base =
|
140
|
+
// eslint-disable-next-line no-nested-ternary
|
141
|
+
typeof previewUrl === 'string'
|
142
|
+
? previewUrl
|
143
|
+
: typeof previewUrl === 'object'
|
144
|
+
? previewUrl?.origin || location.origin
|
145
|
+
: location.origin
|
146
|
+
const url = new URL(relativeUrl, base)
|
139
147
|
|
140
148
|
if (resolvers.length) {
|
141
149
|
let result:
|
@@ -180,7 +188,7 @@ export function useMainDocument(props: {
|
|
180
188
|
setMainDocumentState(undefined)
|
181
189
|
mainDocumentIdRef.current = undefined
|
182
190
|
return undefined
|
183
|
-
}, [client,
|
191
|
+
}, [client, previewUrl, relativeUrl, resolvers, perspectiveStack])
|
184
192
|
|
185
193
|
return mainDocumentState
|
186
194
|
}
|
@@ -3,6 +3,7 @@ import {type MutableRefObject, useCallback, useEffect, useMemo, useRef, useState
|
|
3
3
|
import {getPublishedId} from 'sanity'
|
4
4
|
import {type RouterContextValue, type RouterState, type SearchParam} from 'sanity/router'
|
5
5
|
|
6
|
+
import {parseRouterState} from './lib/parse'
|
6
7
|
import {
|
7
8
|
type CombinedSearchParams,
|
8
9
|
type FrameState,
|
@@ -12,7 +13,6 @@ import {
|
|
12
13
|
type PresentationStateParams,
|
13
14
|
type StructureDocumentPaneParams,
|
14
15
|
} from './types'
|
15
|
-
import {parseRouterState} from './util/parse'
|
16
16
|
|
17
17
|
function pruneObject<T extends RouterState | PresentationParamsContextValue>(obj: T): T {
|
18
18
|
return Object.fromEntries(
|
@@ -50,7 +50,8 @@ export function useParams({
|
|
50
50
|
id,
|
51
51
|
type,
|
52
52
|
path,
|
53
|
-
preview:
|
53
|
+
preview:
|
54
|
+
routerSearchParams.preview || `${initialPreviewUrl.pathname}${initialPreviewUrl.search}`,
|
54
55
|
perspective: routerSearchParams.perspective,
|
55
56
|
viewport: routerSearchParams.viewport,
|
56
57
|
inspect: routerSearchParams.inspect,
|