sanity-plugin-iframe-pane 2.6.1 → 2.6.2-canary.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/README.md +8 -5
- package/lib/index.cjs +167 -187
- package/lib/index.cjs.js +0 -2
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +28 -26
- package/lib/index.js +172 -191
- package/lib/index.js.map +1 -1
- package/package.json +10 -36
- package/src/DisplayUrl.tsx +5 -10
- package/src/Iframe.tsx +227 -175
- package/src/Toolbar.tsx +11 -21
- package/src/index.ts +1 -8
- package/src/types.ts +0 -6
- package/lib/_chunks/is-valid-secret-VKMJU99B.cjs +0 -64
- package/lib/_chunks/is-valid-secret-VKMJU99B.cjs.map +0 -1
- package/lib/_chunks/is-valid-secret-zi24WaHG.js +0 -58
- package/lib/_chunks/is-valid-secret-zi24WaHG.js.map +0 -1
- package/lib/_chunks/utils-HbzA_NjI.cjs +0 -61
- package/lib/_chunks/utils-HbzA_NjI.cjs.map +0 -1
- package/lib/_chunks/utils-j2CLEOFh.js +0 -56
- package/lib/_chunks/utils-j2CLEOFh.js.map +0 -1
- package/lib/is-valid-secret.cjs +0 -8
- package/lib/is-valid-secret.cjs.map +0 -1
- package/lib/is-valid-secret.d.ts +0 -33
- package/lib/is-valid-secret.js +0 -2
- package/lib/is-valid-secret.js.map +0 -1
- package/lib/preview-url.cjs +0 -56
- package/lib/preview-url.cjs.js +0 -4
- package/lib/preview-url.cjs.map +0 -1
- package/lib/preview-url.d.ts +0 -17
- package/lib/preview-url.js +0 -51
- package/lib/preview-url.js.map +0 -1
- package/src/GetUrlSecret.tsx +0 -80
- package/src/defineUrlResolver.tsx +0 -34
- package/src/is-valid-secret.ts +0 -1
- package/src/isValidSecret.tsx +0 -98
- package/src/preview-url.ts +0 -6
- package/src/previewUrl.ts +0 -62
- package/src/utils.ts +0 -45
package/src/Iframe.tsx
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
|
-
/* eslint-disable react/jsx-no-bind */
|
|
2
1
|
import {WarningOutlineIcon} from '@sanity/icons'
|
|
2
|
+
import {createPreviewSecret} from '@sanity/preview-url-secret/create-secret'
|
|
3
|
+
import {definePreviewUrl} from '@sanity/preview-url-secret/define-preview-url'
|
|
3
4
|
import {Box, Card, Container, Flex, Spinner, Stack, Text, usePrefersReducedMotion} from '@sanity/ui'
|
|
4
5
|
import {AnimatePresence, motion, MotionConfig} from 'framer-motion'
|
|
5
|
-
import React, {
|
|
6
|
+
import React, {
|
|
7
|
+
forwardRef,
|
|
8
|
+
memo,
|
|
9
|
+
Suspense,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
useTransition,
|
|
15
|
+
} from 'react'
|
|
6
16
|
import {HTMLAttributeReferrerPolicy} from 'react'
|
|
7
|
-
import {SanityDocument} from 'sanity'
|
|
17
|
+
import {SanityDocument, useClient, useCurrentUser} from 'sanity'
|
|
18
|
+
import {suspend} from 'suspend-react'
|
|
8
19
|
|
|
9
|
-
import {UrlResolver} from './defineUrlResolver'
|
|
10
|
-
import {GetUrlSecret} from './GetUrlSecret'
|
|
11
|
-
import {UrlSecretId} from './isValidSecret'
|
|
12
20
|
import {DEFAULT_SIZE, sizes, Toolbar} from './Toolbar'
|
|
13
|
-
import {IframeSizeKey
|
|
21
|
+
import {IframeSizeKey} from './types'
|
|
14
22
|
|
|
15
|
-
export type
|
|
23
|
+
export type UrlResolver = (
|
|
24
|
+
document: SanityDocument | null,
|
|
25
|
+
) => string | Error | undefined | Promise<string | Error | undefined>
|
|
16
26
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
export type {IframeSizeKey}
|
|
28
|
+
|
|
29
|
+
export interface IframeOptions {
|
|
30
|
+
url:
|
|
31
|
+
| string
|
|
32
|
+
| UrlResolver
|
|
33
|
+
| {
|
|
34
|
+
/**
|
|
35
|
+
* The URL origin of where the preview is hosted, for example `https://example.com`.
|
|
36
|
+
* If it's an embedded Studio then set it to `'same-origin'`.
|
|
37
|
+
*/
|
|
38
|
+
origin: 'same-origin' | string
|
|
39
|
+
/**
|
|
40
|
+
* The route to redirect to after enabling Draft Mode.
|
|
41
|
+
* If you don't have enough data to build the URL, return an `Error` instance to show an error message.
|
|
42
|
+
* @example `return new Error('Missing slug')`
|
|
43
|
+
* To prolong the loading state, return `undefined`
|
|
44
|
+
*/
|
|
45
|
+
preview: string | UrlResolver
|
|
46
|
+
/**
|
|
47
|
+
* The route that enables Draft Mode
|
|
48
|
+
* @example '/api/draft'
|
|
49
|
+
*/
|
|
50
|
+
draftMode: string
|
|
51
|
+
}
|
|
20
52
|
defaultSize?: IframeSizeKey
|
|
21
|
-
loader?: string | boolean
|
|
22
53
|
showDisplayUrl?: boolean
|
|
23
54
|
reload?: {
|
|
24
|
-
revision?: boolean | number
|
|
25
55
|
button?: boolean
|
|
26
56
|
}
|
|
27
57
|
attributes?: Partial<{
|
|
@@ -36,44 +66,155 @@ const MotionFlex = motion(Flex)
|
|
|
36
66
|
|
|
37
67
|
export interface IframeProps {
|
|
38
68
|
document: {
|
|
39
|
-
|
|
69
|
+
draft: SanityDocument | null
|
|
70
|
+
published: SanityDocument | null
|
|
40
71
|
}
|
|
41
72
|
options: IframeOptions
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
export function Iframe(props: IframeProps) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
76
|
+
const {
|
|
77
|
+
document: {published, draft = published},
|
|
78
|
+
options,
|
|
79
|
+
} = props
|
|
80
|
+
const {defaultSize = DEFAULT_SIZE, reload, attributes, showDisplayUrl = true} = options
|
|
81
|
+
|
|
82
|
+
const urlRef = useRef(options.url)
|
|
83
|
+
const [draftSnapshot, setDraftSnapshot] = useState(() => draft)
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
urlRef.current = options.url
|
|
86
|
+
}, [options.url])
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (JSON.stringify(draft) !== JSON.stringify(draftSnapshot)) {
|
|
89
|
+
startTransition(() => setDraftSnapshot(draft))
|
|
90
|
+
}
|
|
91
|
+
}, [draft, draftSnapshot])
|
|
92
|
+
const currentUser = useCurrentUser()
|
|
93
|
+
const client = useClient({apiVersion: '2023-10-16'})
|
|
94
|
+
const [expiresAt, setExpiresAt] = useState<number | undefined>()
|
|
95
|
+
const previewSecretRef = useRef<string | undefined>()
|
|
96
|
+
const [isResolvingUrl, startTransition] = useTransition()
|
|
97
|
+
const url = useCallback(
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
99
|
+
async (draft: SanityDocument | null) => {
|
|
100
|
+
if (typeof location === 'undefined') {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
const urlProp = urlRef.current
|
|
104
|
+
if (typeof urlProp === 'string') {
|
|
105
|
+
return new URL(urlProp, location.origin)
|
|
106
|
+
}
|
|
107
|
+
if (typeof urlProp === 'function') {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
109
|
+
const url = await urlProp(draft)
|
|
110
|
+
return typeof url === 'string' ? new URL(url, location.origin) : url
|
|
111
|
+
}
|
|
112
|
+
if (typeof urlProp === 'object') {
|
|
113
|
+
const preview =
|
|
114
|
+
typeof urlProp.preview === 'function' ? await urlProp.preview(draft) : urlProp.preview
|
|
115
|
+
if (typeof preview !== 'string') {
|
|
116
|
+
return preview
|
|
117
|
+
}
|
|
118
|
+
if (!previewSecretRef.current) {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
120
|
+
const {secret, expiresAt} = await createPreviewSecret(
|
|
121
|
+
client,
|
|
122
|
+
'sanity-plugin-iframe-pane',
|
|
123
|
+
location.href,
|
|
124
|
+
currentUser?.id,
|
|
125
|
+
)
|
|
126
|
+
previewSecretRef.current = secret
|
|
127
|
+
startTransition(() => setExpiresAt(expiresAt.getTime()))
|
|
128
|
+
}
|
|
49
129
|
|
|
50
|
-
|
|
130
|
+
const resolvePreviewUrl = definePreviewUrl({
|
|
131
|
+
origin: urlProp.origin === 'same-origin' ? location.origin : urlProp.origin,
|
|
132
|
+
preview,
|
|
133
|
+
draftMode: {
|
|
134
|
+
enable: urlProp.draftMode,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
138
|
+
const url = await resolvePreviewUrl({
|
|
139
|
+
client,
|
|
140
|
+
previewUrlSecret: previewSecretRef.current,
|
|
141
|
+
previewSearchParam: null,
|
|
142
|
+
})
|
|
143
|
+
return new URL(url, location.origin)
|
|
144
|
+
}
|
|
145
|
+
return undefined
|
|
146
|
+
},
|
|
147
|
+
[client, currentUser?.id],
|
|
148
|
+
)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (expiresAt) {
|
|
151
|
+
const timeout = setTimeout(
|
|
152
|
+
() => {
|
|
153
|
+
startTransition(() => setExpiresAt(undefined))
|
|
154
|
+
previewSecretRef.current = undefined
|
|
155
|
+
},
|
|
156
|
+
Math.max(0, expiresAt - Date.now()),
|
|
157
|
+
)
|
|
158
|
+
return () => clearTimeout(timeout)
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}, [expiresAt])
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Suspense fallback={<Loading iframeSize="desktop" />}>
|
|
165
|
+
<IframeInner
|
|
166
|
+
draftSnapshot={draftSnapshot}
|
|
167
|
+
url={url}
|
|
168
|
+
isResolvingUrl={isResolvingUrl}
|
|
169
|
+
attributes={attributes}
|
|
170
|
+
defaultSize={defaultSize}
|
|
171
|
+
reload={reload}
|
|
172
|
+
showDisplayUrl={showDisplayUrl}
|
|
173
|
+
userId={currentUser?.id}
|
|
174
|
+
/>
|
|
175
|
+
</Suspense>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface IframeInnerProps extends Omit<IframeOptions, 'url'> {
|
|
180
|
+
url: (draftSnapshot: SanityDocument | null) => Promise<URL | Error | undefined>
|
|
181
|
+
isResolvingUrl: boolean
|
|
182
|
+
draftSnapshot: SanityDocument | null
|
|
183
|
+
userId?: string
|
|
184
|
+
expiresAt?: number
|
|
185
|
+
}
|
|
186
|
+
const IframeInner = memo(function IframeInner(props: IframeInnerProps) {
|
|
51
187
|
const {
|
|
52
|
-
|
|
53
|
-
urlSecretId,
|
|
188
|
+
isResolvingUrl,
|
|
54
189
|
defaultSize = DEFAULT_SIZE,
|
|
55
190
|
reload,
|
|
56
|
-
loader = 'Loading…',
|
|
57
191
|
attributes = {},
|
|
58
192
|
showDisplayUrl = true,
|
|
59
|
-
|
|
193
|
+
draftSnapshot,
|
|
194
|
+
userId,
|
|
195
|
+
expiresAt,
|
|
196
|
+
} = props
|
|
60
197
|
const [iframeSize, setIframeSize] = useState(sizes?.[defaultSize] ? defaultSize : DEFAULT_SIZE)
|
|
61
198
|
|
|
62
|
-
// Workaround documents that initially appears to be an empty new document but just hasen't loaded yet
|
|
63
|
-
const [workaroundEmptyDocument, setWorkaroundEmptyDocument] = useState(true)
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const timeout = setTimeout(() => setWorkaroundEmptyDocument(false), 1000)
|
|
66
|
-
return () => clearTimeout(timeout)
|
|
67
|
-
}, [])
|
|
68
|
-
|
|
69
199
|
const prefersReducedMotion = usePrefersReducedMotion()
|
|
70
|
-
|
|
200
|
+
|
|
201
|
+
const url = suspend(
|
|
202
|
+
() => props.url(draftSnapshot),
|
|
203
|
+
[
|
|
204
|
+
// Cache based on a few specific conditions
|
|
205
|
+
'sanity-plugin-iframe-pane',
|
|
206
|
+
draftSnapshot,
|
|
207
|
+
userId,
|
|
208
|
+
expiresAt,
|
|
209
|
+
resolveUUID,
|
|
210
|
+
],
|
|
211
|
+
)
|
|
71
212
|
|
|
72
213
|
const [loading, setLoading] = useState(true)
|
|
73
|
-
const [
|
|
214
|
+
const [_reloading, setReloading] = useState(false)
|
|
215
|
+
const reloading = _reloading || isResolvingUrl
|
|
74
216
|
|
|
75
217
|
const iframe = useRef<HTMLIFrameElement>(null)
|
|
76
|
-
const {displayed} = sanityDocument
|
|
77
218
|
|
|
78
219
|
const handleReload = useCallback(() => {
|
|
79
220
|
if (!iframe?.current) {
|
|
@@ -87,78 +228,52 @@ export function Iframe(props: IframeProps) {
|
|
|
87
228
|
setReloading(true)
|
|
88
229
|
}, [])
|
|
89
230
|
|
|
90
|
-
const deferredRevision = useDeferredValue(displayed._rev)
|
|
91
|
-
const displayUrl = typeof urlState === 'string' ? urlState : ''
|
|
92
|
-
|
|
93
231
|
return (
|
|
94
232
|
<MotionConfig transition={prefersReducedMotion ? {duration: 0} : undefined}>
|
|
95
233
|
<Flex direction="column" style={{height: `100%`}}>
|
|
96
234
|
<Toolbar
|
|
97
|
-
|
|
235
|
+
url={url}
|
|
98
236
|
iframeSize={iframeSize}
|
|
99
237
|
reloading={reloading}
|
|
100
238
|
setIframeSize={setIframeSize}
|
|
101
|
-
|
|
239
|
+
showUrl={showDisplayUrl}
|
|
102
240
|
reloadButton={!!reload?.button}
|
|
103
241
|
handleReload={handleReload}
|
|
104
242
|
/>
|
|
105
|
-
{
|
|
106
|
-
<
|
|
243
|
+
{url instanceof Error ? (
|
|
244
|
+
<ErrorCard error={url} />
|
|
107
245
|
) : (
|
|
108
246
|
<Card tone="transparent" style={{height: `100%`}}>
|
|
109
247
|
<Frame
|
|
110
248
|
ref={iframe}
|
|
111
|
-
loader={loader}
|
|
112
249
|
loading={loading}
|
|
113
250
|
reloading={reloading}
|
|
114
251
|
iframeSize={iframeSize}
|
|
115
252
|
setReloading={setReloading}
|
|
116
253
|
setLoading={setLoading}
|
|
117
|
-
|
|
254
|
+
url={url}
|
|
118
255
|
attributes={attributes}
|
|
119
256
|
/>
|
|
120
257
|
</Card>
|
|
121
258
|
)}
|
|
122
|
-
{typeof url === 'function' && (
|
|
123
|
-
<AsyncUrl
|
|
124
|
-
// We use the revision as a key, to force a re-render when the revision changes
|
|
125
|
-
// This allows us to respond to changed props (maybe the url function itself changes)
|
|
126
|
-
// But avoid calling async logic on every render accidentally
|
|
127
|
-
key={deferredRevision}
|
|
128
|
-
url={url}
|
|
129
|
-
displayed={displayed}
|
|
130
|
-
urlSecretId={urlSecretId}
|
|
131
|
-
setDisplayUrl={setUrlState}
|
|
132
|
-
setError={setError}
|
|
133
|
-
/>
|
|
134
|
-
)}
|
|
135
|
-
{displayUrl && (reload?.revision || reload?.revision === 0) && (
|
|
136
|
-
<ReloadOnRevision
|
|
137
|
-
revision={reload.revision}
|
|
138
|
-
_rev={deferredRevision}
|
|
139
|
-
handleReload={handleReload}
|
|
140
|
-
/>
|
|
141
|
-
)}
|
|
142
259
|
</Flex>
|
|
143
260
|
</MotionConfig>
|
|
144
261
|
)
|
|
145
|
-
}
|
|
262
|
+
})
|
|
146
263
|
|
|
147
|
-
interface FrameProps extends Required<Pick<IframeOptions, '
|
|
148
|
-
loader: string | boolean
|
|
264
|
+
interface FrameProps extends Required<Pick<IframeOptions, 'attributes'>> {
|
|
149
265
|
loading: boolean
|
|
150
266
|
reloading: boolean
|
|
151
267
|
setLoading: (loading: boolean) => void
|
|
152
268
|
setReloading: (reloading: boolean) => void
|
|
153
269
|
iframeSize: IframeSizeKey
|
|
154
|
-
|
|
270
|
+
url: URL | undefined
|
|
155
271
|
}
|
|
156
272
|
const Frame = forwardRef(function Frame(
|
|
157
273
|
props: FrameProps,
|
|
158
274
|
iframe: React.ForwardedRef<HTMLIFrameElement>,
|
|
159
275
|
) {
|
|
160
|
-
const {
|
|
161
|
-
props
|
|
276
|
+
const {loading, setLoading, iframeSize, attributes, reloading, url, setReloading} = props
|
|
162
277
|
|
|
163
278
|
function handleIframeLoad() {
|
|
164
279
|
setLoading(false)
|
|
@@ -172,49 +287,39 @@ const Frame = forwardRef(function Frame(
|
|
|
172
287
|
return (
|
|
173
288
|
<Flex align="center" justify="center" style={{height: `100%`, position: `relative`}}>
|
|
174
289
|
<AnimatePresence>
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
align="center"
|
|
183
|
-
style={{inset: `0`, position: `absolute`}}
|
|
184
|
-
>
|
|
185
|
-
<Flex
|
|
186
|
-
style={{...sizes[iframeSize]}}
|
|
290
|
+
{!url ||
|
|
291
|
+
(loading && (
|
|
292
|
+
<MotionFlex
|
|
293
|
+
initial="initial"
|
|
294
|
+
animate="animate"
|
|
295
|
+
exit="exit"
|
|
296
|
+
variants={spinnerVariants}
|
|
187
297
|
justify="center"
|
|
188
298
|
align="center"
|
|
189
|
-
|
|
190
|
-
gap={4}
|
|
299
|
+
style={{inset: `0`, position: `absolute`}}
|
|
191
300
|
>
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{loader}
|
|
196
|
-
</Text>
|
|
197
|
-
)}
|
|
198
|
-
</Flex>
|
|
199
|
-
</MotionFlex>
|
|
200
|
-
)}
|
|
301
|
+
<Loading iframeSize={iframeSize} />
|
|
302
|
+
</MotionFlex>
|
|
303
|
+
))}
|
|
201
304
|
</AnimatePresence>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
305
|
+
{url && (
|
|
306
|
+
<motion.iframe
|
|
307
|
+
ref={iframe}
|
|
308
|
+
title="preview"
|
|
309
|
+
frameBorder="0"
|
|
310
|
+
style={{maxHeight: '100%'}}
|
|
311
|
+
src={url.toString()}
|
|
312
|
+
initial={['background', iframeSize]}
|
|
313
|
+
variants={iframeVariants}
|
|
314
|
+
animate={[
|
|
315
|
+
loading ? 'background' : 'active',
|
|
316
|
+
reloading ? 'reloading' : 'idle',
|
|
317
|
+
iframeSize,
|
|
318
|
+
]}
|
|
319
|
+
{...attributes}
|
|
320
|
+
onLoad={handleIframeLoad}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
218
323
|
</Flex>
|
|
219
324
|
)
|
|
220
325
|
})
|
|
@@ -251,74 +356,18 @@ const iframeVariants = {
|
|
|
251
356
|
},
|
|
252
357
|
}
|
|
253
358
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// eslint-disable-next-line consistent-return
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
if (_rev !== initialRev) {
|
|
266
|
-
const timeout = setTimeout(handleReload, Number(revision === true ? 300 : revision))
|
|
267
|
-
return () => clearTimeout(timeout)
|
|
268
|
-
}
|
|
269
|
-
}, [_rev, revision, handleReload, initialRev])
|
|
270
|
-
|
|
271
|
-
return null
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
interface AsyncUrlProps {
|
|
275
|
-
displayed: SanityDocument
|
|
276
|
-
url: UrlResolver
|
|
277
|
-
urlSecretId?: UrlSecretId
|
|
278
|
-
setDisplayUrl: (url: UrlState) => void
|
|
279
|
-
setError: SetError
|
|
280
|
-
}
|
|
281
|
-
function AsyncUrl(props: AsyncUrlProps) {
|
|
282
|
-
const {urlSecretId, setDisplayUrl, setError} = props
|
|
283
|
-
// Snapshot values we only care about when the revision changes, done by changing the `key` prop
|
|
284
|
-
const [displayed] = useState(props.displayed)
|
|
285
|
-
const [url] = useState(() => props.url)
|
|
286
|
-
const [urlSecret, setUrlSecret] = useState<null | string>(null)
|
|
287
|
-
|
|
288
|
-
// Set initial URL and refresh on new revisions
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
if (urlSecretId && !urlSecret) return
|
|
291
|
-
|
|
292
|
-
const getUrl = async (signal: AbortSignal) => {
|
|
293
|
-
const resolveUrl = await url(displayed, urlSecret, abort.signal)
|
|
294
|
-
|
|
295
|
-
// Only update state if URL has changed
|
|
296
|
-
if (!signal.aborted && resolveUrl) {
|
|
297
|
-
setDisplayUrl(resolveUrl)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const abort = new AbortController()
|
|
302
|
-
getUrl(abort.signal).catch((error) => error.name !== 'AbortError' && setError(error))
|
|
303
|
-
// eslint-disable-next-line consistent-return
|
|
304
|
-
return () => abort.abort()
|
|
305
|
-
}, [displayed, setDisplayUrl, setError, url, urlSecret, urlSecretId])
|
|
306
|
-
|
|
307
|
-
if (urlSecretId) {
|
|
308
|
-
return (
|
|
309
|
-
<GetUrlSecret
|
|
310
|
-
urlSecretId={urlSecretId}
|
|
311
|
-
urlSecret={urlSecret}
|
|
312
|
-
setUrlSecret={setUrlSecret}
|
|
313
|
-
setError={setError}
|
|
314
|
-
/>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return null
|
|
359
|
+
function Loading({iframeSize}: {iframeSize: IframeSizeKey}) {
|
|
360
|
+
return (
|
|
361
|
+
<Flex style={{...sizes[iframeSize]}} justify="center" align="center" direction="column" gap={4}>
|
|
362
|
+
<Spinner muted />
|
|
363
|
+
<Text muted size={1}>
|
|
364
|
+
Loading…
|
|
365
|
+
</Text>
|
|
366
|
+
</Flex>
|
|
367
|
+
)
|
|
319
368
|
}
|
|
320
369
|
|
|
321
|
-
export function
|
|
370
|
+
export function ErrorCard({error}: {error: Error}) {
|
|
322
371
|
return (
|
|
323
372
|
<Card height="fill">
|
|
324
373
|
<Flex align="center" height="fill" justify="center" padding={4} sizing="border">
|
|
@@ -332,10 +381,10 @@ export function MissingSlugScreen() {
|
|
|
332
381
|
</Box>
|
|
333
382
|
<Stack flex={1} marginLeft={3} space={3}>
|
|
334
383
|
<Text as="h1" size={1} weight="bold">
|
|
335
|
-
|
|
384
|
+
{error.name}
|
|
336
385
|
</Text>
|
|
337
386
|
<Text as="p" muted size={1}>
|
|
338
|
-
|
|
387
|
+
{error.message}
|
|
339
388
|
</Text>
|
|
340
389
|
</Stack>
|
|
341
390
|
</Flex>
|
|
@@ -345,3 +394,6 @@ export function MissingSlugScreen() {
|
|
|
345
394
|
</Card>
|
|
346
395
|
)
|
|
347
396
|
}
|
|
397
|
+
|
|
398
|
+
// https://github.com/pmndrs/suspend-react?tab=readme-ov-file#making-cache-keys-unique
|
|
399
|
+
const resolveUUID = Symbol()
|
package/src/Toolbar.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable react/jsx-no-bind */
|
|
2
1
|
import {CopyIcon, LaunchIcon, MobileDeviceIcon, RefreshIcon} from '@sanity/icons'
|
|
3
2
|
import {Box, Button, Card, Flex, Text, Tooltip, useToast} from '@sanity/ui'
|
|
4
3
|
import React, {useRef} from 'react'
|
|
@@ -21,24 +20,17 @@ export const sizes: SizeProps = {
|
|
|
21
20
|
export const DEFAULT_SIZE = `desktop`
|
|
22
21
|
|
|
23
22
|
export interface ToolbarProps {
|
|
24
|
-
|
|
23
|
+
url: URL | Error | undefined
|
|
25
24
|
iframeSize: IframeSizeKey
|
|
26
25
|
setIframeSize: (size: IframeSizeKey) => void
|
|
27
|
-
|
|
26
|
+
showUrl: boolean
|
|
28
27
|
reloading: boolean
|
|
29
28
|
reloadButton: boolean
|
|
30
29
|
handleReload: () => void
|
|
31
30
|
}
|
|
32
31
|
export function Toolbar(props: ToolbarProps) {
|
|
33
|
-
const {
|
|
34
|
-
|
|
35
|
-
iframeSize,
|
|
36
|
-
setIframeSize,
|
|
37
|
-
reloading,
|
|
38
|
-
showDisplayUrl,
|
|
39
|
-
reloadButton,
|
|
40
|
-
handleReload,
|
|
41
|
-
} = props
|
|
32
|
+
const {url, iframeSize, setIframeSize, reloading, showUrl, reloadButton, handleReload} = props
|
|
33
|
+
const validUrl = url instanceof URL
|
|
42
34
|
|
|
43
35
|
const input = useRef<HTMLTextAreaElement>(null)
|
|
44
36
|
const {push: pushToast} = useToast()
|
|
@@ -49,7 +41,7 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
49
41
|
<textarea
|
|
50
42
|
style={{position: `absolute`, pointerEvents: `none`, opacity: 0}}
|
|
51
43
|
ref={input}
|
|
52
|
-
value={
|
|
44
|
+
value={validUrl ? url.toString() : ''}
|
|
53
45
|
readOnly
|
|
54
46
|
tabIndex={-1}
|
|
55
47
|
/>
|
|
@@ -66,7 +58,7 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
66
58
|
placement="bottom-start"
|
|
67
59
|
>
|
|
68
60
|
<Button
|
|
69
|
-
disabled={!
|
|
61
|
+
disabled={!validUrl}
|
|
70
62
|
fontSize={[1]}
|
|
71
63
|
padding={2}
|
|
72
64
|
mode={iframeSize === 'mobile' ? 'default' : 'ghost'}
|
|
@@ -75,9 +67,7 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
75
67
|
/>
|
|
76
68
|
</Tooltip>
|
|
77
69
|
</Flex>
|
|
78
|
-
<Box flex={1}>
|
|
79
|
-
{showDisplayUrl && displayUrl && <DisplayUrl displayUrl={displayUrl} />}
|
|
80
|
-
</Box>
|
|
70
|
+
<Box flex={1}>{showUrl && validUrl && <DisplayUrl url={url} />}</Box>
|
|
81
71
|
<Flex align="center" gap={1}>
|
|
82
72
|
{reloadButton ? (
|
|
83
73
|
<Tooltip
|
|
@@ -89,7 +79,7 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
89
79
|
padding={2}
|
|
90
80
|
>
|
|
91
81
|
<Button
|
|
92
|
-
disabled={!
|
|
82
|
+
disabled={!validUrl}
|
|
93
83
|
mode="bleed"
|
|
94
84
|
fontSize={[1]}
|
|
95
85
|
padding={2}
|
|
@@ -110,7 +100,7 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
110
100
|
>
|
|
111
101
|
<Button
|
|
112
102
|
mode="bleed"
|
|
113
|
-
disabled={!
|
|
103
|
+
disabled={!validUrl}
|
|
114
104
|
fontSize={[1]}
|
|
115
105
|
icon={CopyIcon}
|
|
116
106
|
padding={[2]}
|
|
@@ -137,14 +127,14 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
137
127
|
placement="bottom-end"
|
|
138
128
|
>
|
|
139
129
|
<Button
|
|
140
|
-
disabled={!
|
|
130
|
+
disabled={!validUrl}
|
|
141
131
|
fontSize={[1]}
|
|
142
132
|
icon={LaunchIcon}
|
|
143
133
|
mode="ghost"
|
|
144
134
|
paddingY={[2]}
|
|
145
135
|
text="Open"
|
|
146
136
|
aria-label="Open URL in a new tab"
|
|
147
|
-
onClick={() => window.open(
|
|
137
|
+
onClick={validUrl ? () => window.open(url.toString()) : undefined}
|
|
148
138
|
/>
|
|
149
139
|
</Tooltip>
|
|
150
140
|
</Flex>
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
|
-
defineUrlResolver,
|
|
3
|
-
type DefineUrlResolverOptions,
|
|
4
|
-
type UrlResolver,
|
|
5
|
-
type UrlState,
|
|
6
|
-
} from './defineUrlResolver'
|
|
7
|
-
export {
|
|
8
|
-
Iframe as default,
|
|
9
2
|
Iframe,
|
|
10
3
|
type IframeOptions,
|
|
11
4
|
type IframeProps,
|
|
12
5
|
type IframeSizeKey,
|
|
13
|
-
type
|
|
6
|
+
type UrlResolver,
|
|
14
7
|
} from './Iframe'
|
|
15
8
|
export type {Size, SizeProps} from './types'
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export const MissingSlug = Symbol('MissingSlug')
|
|
2
|
-
|
|
3
|
-
export type UrlState = string | typeof MissingSlug
|
|
4
|
-
|
|
5
1
|
export type IframeSizeKey = keyof SizeProps
|
|
6
2
|
|
|
7
3
|
export type Size = 'desktop' | 'mobile'
|
|
@@ -13,5 +9,3 @@ export type SizeProps = {
|
|
|
13
9
|
height: string | number
|
|
14
10
|
}
|
|
15
11
|
}
|
|
16
|
-
|
|
17
|
-
export type SetError = (error: unknown) => void
|