sanity-plugin-iframe-pane 2.6.1 → 3.0.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/index.js CHANGED
@@ -1,84 +1,22 @@
1
- import { patchUrlSecret, getExpiresAt, MissingSlug } from './_chunks/utils-j2CLEOFh.js';
2
- export { defineUrlResolver } from './_chunks/utils-j2CLEOFh.js';
3
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
2
  import { MobileDeviceIcon, RefreshIcon, CopyIcon, LaunchIcon, WarningOutlineIcon } from '@sanity/icons';
5
- import { Text, useToast, Card, Flex, Tooltip, Button, Box, Spinner, usePrefersReducedMotion, Container, Stack } from '@sanity/ui';
6
- import { motion, AnimatePresence, MotionConfig } from 'framer-motion';
7
- import { useState, useEffect, useMemo, useRef, forwardRef, useCallback, useDeferredValue } from 'react';
8
- import { useClient } from 'sanity';
9
- import { apiVersion, fetchSecretQuery, tag } from './_chunks/is-valid-secret-zi24WaHG.js';
3
+ import { createPreviewSecret } from '@sanity/preview-url-secret/create-secret';
4
+ import { definePreviewUrl } from '@sanity/preview-url-secret/define-preview-url';
5
+ import { Text, useToast, Card, Flex, Tooltip, Button, Box, usePrefersReducedMotion, Spinner, Container, Stack } from '@sanity/ui';
6
+ import { motion, MotionConfig, AnimatePresence } from 'framer-motion';
7
+ import { useMemo, useRef, memo, useState, useCallback, forwardRef, useEffect, useTransition, Suspense } from 'react';
8
+ import { useCurrentUser, useClient } from 'sanity';
9
+ import { suspend } from 'suspend-react';
10
10
  import { useCopyToClipboard } from 'usehooks-ts';
11
- function GetUrlSecret(props) {
12
- const {
13
- urlSecretId,
14
- setUrlSecret,
15
- urlSecret,
16
- setError
17
- } = props;
18
- const client = useClient({
19
- apiVersion
20
- });
21
- const [secretExpiresAt, setSecretExpiresAt] = useState(null);
22
- if (!urlSecretId.includes(".")) {
23
- throw new TypeError("`urlSecretId` must have a dot prefix, `".concat(urlSecretId, "` is not secure, add a prefix, for example `preview.").concat(urlSecretId, "` "));
24
- }
25
- useEffect(() => {
26
- if (urlSecret) return;
27
- async function getSecret(signal) {
28
- const data = await client.fetch(fetchSecretQuery, {
29
- id: urlSecretId
30
- }, {
31
- signal,
32
- tag
33
- });
34
- if (signal.aborted) return;
35
- if (!(data == null ? void 0 : data.secret) || !(data == null ? void 0 : data._updatedAt)) {
36
- try {
37
- const newUpdatedAt = /* @__PURE__ */new Date();
38
- const newSecret = await patchUrlSecret(client, urlSecretId, signal);
39
- if (signal.aborted) return;
40
- setUrlSecret(newSecret);
41
- setSecretExpiresAt(getExpiresAt(newUpdatedAt));
42
- } catch (err) {
43
- console.error("Failed to create a new preview secret. Ensure the `client` has a `token` specified that has `write` permissions.", err);
44
- }
45
- return;
46
- }
47
- if ((data == null ? void 0 : data.secret) !== urlSecret) {
48
- setUrlSecret(data == null ? void 0 : data.secret);
49
- setSecretExpiresAt(getExpiresAt(new Date(data == null ? void 0 : data._updatedAt)));
50
- }
51
- }
52
- const abort = new AbortController();
53
- getSecret(abort.signal).catch(error => error.name !== "AbortError" && setError(error));
54
- return () => abort.abort();
55
- }, [client, setError, setUrlSecret, urlSecret, urlSecretId]);
56
- useEffect(() => {
57
- if (!secretExpiresAt) return;
58
- const timeout = setTimeout(() => {
59
- setUrlSecret(null);
60
- setSecretExpiresAt(null);
61
- }, Math.max(0, secretExpiresAt.getTime() - /* @__PURE__ */new Date().getTime()));
62
- return () => clearTimeout(timeout);
63
- }, [secretExpiresAt, setUrlSecret]);
64
- return null;
65
- }
66
- function DisplayUrl(_ref) {
67
- let {
68
- displayUrl
69
- } = _ref;
11
+ import { getRedirectTo } from '@sanity/preview-url-secret/get-redirect-to';
12
+ function DisplayUrl(props) {
70
13
  const truncatedUrl = useMemo(() => {
71
- const url = new URL(displayUrl);
72
- if (url.searchParams.has("secret")) {
73
- url.searchParams.delete("secret");
74
- url.searchParams.append("secret", "***");
75
- }
14
+ const url = getRedirectTo(props.url);
76
15
  return "".concat(url.origin === location.origin ? "" : url.origin).concat(url.pathname).concat(url.search);
77
- }, [displayUrl]);
16
+ }, [props.url]);
78
17
  return /* @__PURE__ */jsx(Text, {
79
18
  size: 0,
80
19
  textOverflow: "ellipsis",
81
- title: displayUrl,
82
20
  children: truncatedUrl
83
21
  });
84
22
  }
@@ -95,14 +33,15 @@ const sizes = {
95
33
  const DEFAULT_SIZE = "desktop";
96
34
  function Toolbar(props) {
97
35
  const {
98
- displayUrl,
36
+ url,
99
37
  iframeSize,
100
38
  setIframeSize,
101
39
  reloading,
102
- showDisplayUrl,
40
+ showUrl,
103
41
  reloadButton,
104
42
  handleReload
105
43
  } = props;
44
+ const validUrl = url instanceof URL;
106
45
  const input = useRef(null);
107
46
  const {
108
47
  push: pushToast
@@ -116,7 +55,7 @@ function Toolbar(props) {
116
55
  opacity: 0
117
56
  },
118
57
  ref: input,
119
- value: displayUrl,
58
+ value: validUrl ? url.toString() : "",
120
59
  readOnly: true,
121
60
  tabIndex: -1
122
61
  }), /* @__PURE__ */jsx(Card, {
@@ -139,7 +78,7 @@ function Toolbar(props) {
139
78
  padding: 2,
140
79
  placement: "bottom-start",
141
80
  children: /* @__PURE__ */jsx(Button, {
142
- disabled: !displayUrl,
81
+ disabled: !validUrl,
143
82
  fontSize: [1],
144
83
  padding: 2,
145
84
  mode: iframeSize === "mobile" ? "default" : "ghost",
@@ -149,8 +88,8 @@ function Toolbar(props) {
149
88
  })
150
89
  }), /* @__PURE__ */jsx(Box, {
151
90
  flex: 1,
152
- children: showDisplayUrl && displayUrl && /* @__PURE__ */jsx(DisplayUrl, {
153
- displayUrl
91
+ children: showUrl && validUrl && /* @__PURE__ */jsx(DisplayUrl, {
92
+ url
154
93
  })
155
94
  }), /* @__PURE__ */jsxs(Flex, {
156
95
  align: "center",
@@ -165,7 +104,7 @@ function Toolbar(props) {
165
104
  }),
166
105
  padding: 2,
167
106
  children: /* @__PURE__ */jsx(Button, {
168
- disabled: !displayUrl,
107
+ disabled: !validUrl,
169
108
  mode: "bleed",
170
109
  fontSize: [1],
171
110
  padding: 2,
@@ -185,7 +124,7 @@ function Toolbar(props) {
185
124
  padding: 2,
186
125
  children: /* @__PURE__ */jsx(Button, {
187
126
  mode: "bleed",
188
- disabled: !displayUrl,
127
+ disabled: !validUrl,
189
128
  fontSize: [1],
190
129
  icon: CopyIcon,
191
130
  padding: [2],
@@ -212,14 +151,14 @@ function Toolbar(props) {
212
151
  padding: 2,
213
152
  placement: "bottom-end",
214
153
  children: /* @__PURE__ */jsx(Button, {
215
- disabled: !displayUrl,
154
+ disabled: !validUrl,
216
155
  fontSize: [1],
217
156
  icon: LaunchIcon,
218
157
  mode: "ghost",
219
158
  paddingY: [2],
220
159
  text: "Open",
221
160
  "aria-label": "Open URL in a new tab",
222
- onClick: () => window.open(displayUrl)
161
+ onClick: validUrl ? () => window.open(url.toString()) : void 0
223
162
  })
224
163
  })]
225
164
  })]
@@ -229,38 +168,126 @@ function Toolbar(props) {
229
168
  }
230
169
  const MotionFlex = motion(Flex);
231
170
  function Iframe(props) {
232
- var _a;
233
- const [error, setError] = useState(null);
234
- if (error) {
235
- throw error;
236
- }
237
171
  const {
238
- document: sanityDocument,
172
+ document: {
173
+ published,
174
+ draft = published
175
+ },
239
176
  options
240
177
  } = props;
241
178
  const {
242
- url,
243
- urlSecretId,
244
179
  defaultSize = DEFAULT_SIZE,
245
180
  reload,
246
- loader = "Loading\u2026",
247
- attributes = {},
181
+ attributes,
248
182
  showDisplayUrl = true
249
183
  } = options;
250
- const [iframeSize, setIframeSize] = useState(((_a = sizes) == null ? void 0 : _a[defaultSize]) ? defaultSize : DEFAULT_SIZE);
251
- const [workaroundEmptyDocument, setWorkaroundEmptyDocument] = useState(true);
184
+ const urlRef = useRef(options.url);
185
+ const [draftSnapshot, setDraftSnapshot] = useState(() => draft);
252
186
  useEffect(() => {
253
- const timeout = setTimeout(() => setWorkaroundEmptyDocument(false), 1e3);
254
- return () => clearTimeout(timeout);
255
- }, []);
187
+ urlRef.current = options.url;
188
+ }, [options.url]);
189
+ useEffect(() => {
190
+ if (JSON.stringify(draft) !== JSON.stringify(draftSnapshot)) {
191
+ startTransition(() => setDraftSnapshot(draft));
192
+ }
193
+ }, [draft, draftSnapshot]);
194
+ const currentUser = useCurrentUser();
195
+ const client = useClient({
196
+ apiVersion: "2023-10-16"
197
+ });
198
+ const [expiresAt, setExpiresAt] = useState();
199
+ const previewSecretRef = useRef();
200
+ const [isResolvingUrl, startTransition] = useTransition();
201
+ const url = useCallback(
202
+ // eslint-disable-next-line @typescript-eslint/no-shadow
203
+ async draft2 => {
204
+ if (typeof location === "undefined") {
205
+ return void 0;
206
+ }
207
+ const urlProp = urlRef.current;
208
+ if (typeof urlProp === "string") {
209
+ return new URL(urlProp, location.origin);
210
+ }
211
+ if (typeof urlProp === "function") {
212
+ const url2 = await urlProp(draft2);
213
+ return typeof url2 === "string" ? new URL(url2, location.origin) : url2;
214
+ }
215
+ if (typeof urlProp === "object") {
216
+ const preview = typeof urlProp.preview === "function" ? await urlProp.preview(draft2) : urlProp.preview;
217
+ if (typeof preview !== "string") {
218
+ return preview;
219
+ }
220
+ if (!previewSecretRef.current) {
221
+ const {
222
+ secret,
223
+ expiresAt: expiresAt2
224
+ } = await createPreviewSecret(client, "sanity-plugin-iframe-pane", location.href, currentUser == null ? void 0 : currentUser.id);
225
+ previewSecretRef.current = secret;
226
+ startTransition(() => setExpiresAt(expiresAt2.getTime()));
227
+ }
228
+ const resolvePreviewUrl = definePreviewUrl({
229
+ origin: urlProp.origin === "same-origin" ? location.origin : urlProp.origin,
230
+ preview,
231
+ draftMode: {
232
+ enable: urlProp.draftMode
233
+ }
234
+ });
235
+ const url2 = await resolvePreviewUrl({
236
+ client,
237
+ previewUrlSecret: previewSecretRef.current,
238
+ previewSearchParam: null
239
+ });
240
+ return new URL(url2, location.origin);
241
+ }
242
+ return void 0;
243
+ }, [client, currentUser == null ? void 0 : currentUser.id]);
244
+ useEffect(() => {
245
+ if (expiresAt) {
246
+ const timeout = setTimeout(() => {
247
+ startTransition(() => setExpiresAt(void 0));
248
+ previewSecretRef.current = void 0;
249
+ }, Math.max(0, expiresAt - Date.now()));
250
+ return () => clearTimeout(timeout);
251
+ }
252
+ return void 0;
253
+ }, [expiresAt]);
254
+ return /* @__PURE__ */jsx(Suspense, {
255
+ fallback: /* @__PURE__ */jsx(Loading, {
256
+ iframeSize: "desktop"
257
+ }),
258
+ children: /* @__PURE__ */jsx(IframeInner, {
259
+ draftSnapshot,
260
+ url,
261
+ isResolvingUrl,
262
+ attributes,
263
+ defaultSize,
264
+ reload,
265
+ showDisplayUrl,
266
+ userId: currentUser == null ? void 0 : currentUser.id
267
+ })
268
+ });
269
+ }
270
+ const IframeInner = memo(function IframeInner2(props) {
271
+ var _a;
272
+ const {
273
+ isResolvingUrl,
274
+ defaultSize = DEFAULT_SIZE,
275
+ reload,
276
+ attributes = {},
277
+ showDisplayUrl = true,
278
+ draftSnapshot,
279
+ userId,
280
+ expiresAt
281
+ } = props;
282
+ const [iframeSize, setIframeSize] = useState(((_a = sizes) == null ? void 0 : _a[defaultSize]) ? defaultSize : DEFAULT_SIZE);
256
283
  const prefersReducedMotion = usePrefersReducedMotion();
257
- const [urlState, setUrlState] = useState(() => typeof url === "function" ? "" : url);
284
+ const url = suspend(() => props.url(draftSnapshot), [
285
+ // Cache based on a few specific conditions
286
+ "sanity-plugin-iframe-pane", draftSnapshot, userId, expiresAt, resolveUUID]);
258
287
  const [loading, setLoading] = useState(true);
259
- const [reloading, setReloading] = useState(false);
288
+ const [_reloading, setReloading] = useState(false);
289
+ const reloading = _reloading || isResolvingUrl;
260
290
  const iframe = useRef(null);
261
- const {
262
- displayed
263
- } = sanityDocument;
264
291
  const handleReload = useCallback(() => {
265
292
  if (!(iframe == null ? void 0 : iframe.current)) {
266
293
  return;
@@ -268,8 +295,6 @@ function Iframe(props) {
268
295
  iframe.current.src = iframe.current.src;
269
296
  setReloading(true);
270
297
  }, []);
271
- const deferredRevision = useDeferredValue(displayed._rev);
272
- const displayUrl = typeof urlState === "string" ? urlState : "";
273
298
  return /* @__PURE__ */jsx(MotionConfig, {
274
299
  transition: prefersReducedMotion ? {
275
300
  duration: 0
@@ -280,52 +305,42 @@ function Iframe(props) {
280
305
  height: "100%"
281
306
  },
282
307
  children: [/* @__PURE__ */jsx(Toolbar, {
283
- displayUrl,
308
+ url,
284
309
  iframeSize,
285
310
  reloading,
286
311
  setIframeSize,
287
- showDisplayUrl,
312
+ showUrl: showDisplayUrl,
288
313
  reloadButton: !!(reload == null ? void 0 : reload.button),
289
314
  handleReload
290
- }), urlState === MissingSlug && !workaroundEmptyDocument ? /* @__PURE__ */jsx(MissingSlugScreen, {}) : /* @__PURE__ */jsx(Card, {
315
+ }), url instanceof Error ? /* @__PURE__ */jsx(ErrorCard, {
316
+ error: url
317
+ }) : /* @__PURE__ */jsx(Card, {
291
318
  tone: "transparent",
292
319
  style: {
293
320
  height: "100%"
294
321
  },
295
322
  children: /* @__PURE__ */jsx(Frame, {
296
323
  ref: iframe,
297
- loader,
298
324
  loading,
299
325
  reloading,
300
326
  iframeSize,
301
327
  setReloading,
302
328
  setLoading,
303
- displayUrl,
329
+ url,
304
330
  attributes
305
331
  })
306
- }), typeof url === "function" && /* @__PURE__ */jsx(AsyncUrl, {
307
- url,
308
- displayed,
309
- urlSecretId,
310
- setDisplayUrl: setUrlState,
311
- setError
312
- }, deferredRevision), displayUrl && ((reload == null ? void 0 : reload.revision) || (reload == null ? void 0 : reload.revision) === 0) && /* @__PURE__ */jsx(ReloadOnRevision, {
313
- revision: reload.revision,
314
- _rev: deferredRevision,
315
- handleReload
316
332
  })]
317
333
  })
318
334
  });
319
- }
335
+ });
320
336
  const Frame = forwardRef(function Frame2(props, iframe) {
321
337
  const {
322
- loader,
323
338
  loading,
324
339
  setLoading,
325
340
  iframeSize,
326
341
  attributes,
327
342
  reloading,
328
- displayUrl,
343
+ url,
329
344
  setReloading
330
345
  } = props;
331
346
  function handleIframeLoad() {
@@ -343,7 +358,7 @@ const Frame = forwardRef(function Frame2(props, iframe) {
343
358
  position: "relative"
344
359
  },
345
360
  children: [/* @__PURE__ */jsx(AnimatePresence, {
346
- children: loader && loading && /* @__PURE__ */jsx(MotionFlex, {
361
+ children: !url || loading && /* @__PURE__ */jsx(MotionFlex, {
347
362
  initial: "initial",
348
363
  animate: "animate",
349
364
  exit: "exit",
@@ -354,34 +369,21 @@ const Frame = forwardRef(function Frame2(props, iframe) {
354
369
  inset: "0",
355
370
  position: "absolute"
356
371
  },
357
- children: /* @__PURE__ */jsxs(Flex, {
358
- style: {
359
- ...sizes[iframeSize]
360
- },
361
- justify: "center",
362
- align: "center",
363
- direction: "column",
364
- gap: 4,
365
- children: [/* @__PURE__ */jsx(Spinner, {
366
- muted: true
367
- }), loader && typeof loader === "string" && /* @__PURE__ */jsx(Text, {
368
- muted: true,
369
- size: 1,
370
- children: loader
371
- })]
372
+ children: /* @__PURE__ */jsx(Loading, {
373
+ iframeSize
372
374
  })
373
375
  })
374
- }), /* @__PURE__ */jsx(motion.iframe, {
376
+ }), url && /* @__PURE__ */jsx(motion.iframe, {
375
377
  ref: iframe,
376
378
  title: "preview",
377
379
  frameBorder: "0",
378
380
  style: {
379
381
  maxHeight: "100%"
380
382
  },
381
- src: displayUrl,
383
+ src: url.toString(),
382
384
  initial: ["background", iframeSize],
383
385
  variants: iframeVariants,
384
- animate: [loader && loading ? "background" : "active", reloading ? "reloading" : "idle", iframeSize],
386
+ animate: [loading ? "background" : "active", reloading ? "reloading" : "idle", iframeSize],
385
387
  ...attributes,
386
388
  onLoad: handleIframeLoad
387
389
  })]
@@ -423,53 +425,31 @@ const iframeVariants = {
423
425
  scale: 1
424
426
  }
425
427
  };
426
- function ReloadOnRevision(props) {
427
- const {
428
- revision,
429
- handleReload,
430
- _rev
431
- } = props;
432
- const [initialRev] = useState(_rev);
433
- useEffect(() => {
434
- if (_rev !== initialRev) {
435
- const timeout = setTimeout(handleReload, Number(revision === true ? 300 : revision));
436
- return () => clearTimeout(timeout);
437
- }
438
- }, [_rev, revision, handleReload, initialRev]);
439
- return null;
440
- }
441
- function AsyncUrl(props) {
442
- const {
443
- urlSecretId,
444
- setDisplayUrl,
445
- setError
446
- } = props;
447
- const [displayed] = useState(props.displayed);
448
- const [url] = useState(() => props.url);
449
- const [urlSecret, setUrlSecret] = useState(null);
450
- useEffect(() => {
451
- if (urlSecretId && !urlSecret) return;
452
- const getUrl = async signal => {
453
- const resolveUrl = await url(displayed, urlSecret, abort.signal);
454
- if (!signal.aborted && resolveUrl) {
455
- setDisplayUrl(resolveUrl);
456
- }
457
- };
458
- const abort = new AbortController();
459
- getUrl(abort.signal).catch(error => error.name !== "AbortError" && setError(error));
460
- return () => abort.abort();
461
- }, [displayed, setDisplayUrl, setError, url, urlSecret, urlSecretId]);
462
- if (urlSecretId) {
463
- return /* @__PURE__ */jsx(GetUrlSecret, {
464
- urlSecretId,
465
- urlSecret,
466
- setUrlSecret,
467
- setError
468
- });
469
- }
470
- return null;
428
+ function Loading(_ref) {
429
+ let {
430
+ iframeSize
431
+ } = _ref;
432
+ return /* @__PURE__ */jsxs(Flex, {
433
+ style: {
434
+ ...sizes[iframeSize]
435
+ },
436
+ justify: "center",
437
+ align: "center",
438
+ direction: "column",
439
+ gap: 4,
440
+ children: [/* @__PURE__ */jsx(Spinner, {
441
+ muted: true
442
+ }), /* @__PURE__ */jsx(Text, {
443
+ muted: true,
444
+ size: 1,
445
+ children: "Loading\u2026"
446
+ })]
447
+ });
471
448
  }
472
- function MissingSlugScreen() {
449
+ function ErrorCard(_ref2) {
450
+ let {
451
+ error
452
+ } = _ref2;
473
453
  return /* @__PURE__ */jsx(Card, {
474
454
  height: "fill",
475
455
  children: /* @__PURE__ */jsx(Flex, {
@@ -499,12 +479,12 @@ function MissingSlugScreen() {
499
479
  as: "h1",
500
480
  size: 1,
501
481
  weight: "bold",
502
- children: "Missing slug"
482
+ children: error.name
503
483
  }), /* @__PURE__ */jsx(Text, {
504
484
  as: "p",
505
485
  muted: true,
506
486
  size: 1,
507
- children: "Add a slug to see the preview."
487
+ children: error.message
508
488
  })]
509
489
  })]
510
490
  })
@@ -513,5 +493,6 @@ function MissingSlugScreen() {
513
493
  })
514
494
  });
515
495
  }
516
- export { Iframe, Iframe as default };
496
+ const resolveUUID = Symbol();
497
+ export { Iframe };
517
498
  //# sourceMappingURL=index.js.map