tinacms 3.12.1 → 3.13.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/dist/index.js CHANGED
@@ -20,8 +20,7 @@ import { SlashInputPlugin, SlashPlugin } from "@udecode/plate-slash-command/reac
20
20
  import { TablePlugin, TableRowPlugin, useTableCellElement, TableProvider, useTableElement, useTableMergeState, useTableBordersDropdownMenuContentState, TableCellPlugin, TableCellHeaderPlugin } from "@udecode/plate-table/react";
21
21
  import { PlateElement, useReadOnly, useElement, PlateLeaf, useSelected, useFocused, useEditorRef, withRef as withRef$1, useComposedRef, useEditorPlugin, useElementSelector, withHOC, usePluginOption, useRemoveNodeButton, useEditorSelector, ParagraphPlugin, createPlatePlugin, PlateContent, PlateContainer, useEditorState, useMarkToolbarButtonState, useMarkToolbarButton, usePlateEditor, useFormInputProps, useEditorSelection, useEditorReadOnly, useOnClickOutside, useEventEditorValue, Plate } from "@udecode/plate/react";
22
22
  import { isLangSupported, formatCodeBlock, insertEmptyCodeBlock, isCodeBlockEmpty, unwrapCodeBlock, isSelectionAtCodeBlockStart } from "@udecode/plate-code-block";
23
- import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Heading6 as Heading6$1, Heading5 as Heading5$1, Heading4 as Heading4$1, Heading3 as Heading3$1, Heading2 as Heading2$1, Heading1 as Heading1$1, ChevronRight, Twitter, SunMedium, Moon, SeparatorHorizontal, Eye, Link2Off, Underline, Trash, Text, Table as Table$1, Superscript, Subscript, Strikethrough, Highlighter, Settings, RectangleHorizontal, RotateCcw, Pilcrow, Outdent, MoreHorizontal, Minus, WrapText, Keyboard, Indent, ExternalLink, Smile, Edit2, GripVertical, MessageSquarePlus, MessageSquare, Ungroup, Combine, RectangleVertical, Baseline, FileCode, PaintBucket, ChevronsUpDown, Quote, AlignRight as AlignRight$1, AlignLeft as AlignLeft$1, AlignJustify, AlignCenter as AlignCenter$1, Plus, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, Info, CircleAlert, CircleCheck as CircleCheck$1, ChevronDownIcon as ChevronDownIcon$1, File as File$1, ChevronUp, Edit, Pencil, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, WifiOff, TriangleAlert, RefreshCw, GitBranch, CircleHelp, Loader2, Lock, ArrowUpDown, GitFork, Globe, Copy, Folder as Folder$1, Clapperboard, LayoutGrid, List as List$1, CloudUpload, ArrowDownToLine, TextCursorInput, Images, AlertCircle, Image, Database, CheckCircle2, ImageOff, KeyRound, Cloud, LoaderCircle, ScrollText, RefreshCwOff, OctagonAlert, Menu, FilePlus, LogOut, FileStack, FolderOpen, History, Users, PanelLeft, Files, User, FileText, GitBranchIcon, Circle as Circle$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink, SquarePen, Trash2, House, Unlock } from "lucide-react";
24
- import mermaid from "mermaid";
23
+ import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Heading6 as Heading6$1, Heading5 as Heading5$1, Heading4 as Heading4$1, Heading3 as Heading3$1, Heading2 as Heading2$1, Heading1 as Heading1$1, ChevronRight, Twitter, SunMedium, Moon, SeparatorHorizontal, Eye, Link2Off, Underline, Trash, Text, Table as Table$1, Superscript, Subscript, Strikethrough, Highlighter, Settings, RectangleHorizontal, RotateCcw, Pilcrow, Outdent, MoreHorizontal, Minus, WrapText, Keyboard, Indent, ExternalLink, Smile, Edit2, GripVertical, MessageSquarePlus, MessageSquare, Ungroup, Combine, RectangleVertical, Baseline, FileCode, PaintBucket, ChevronsUpDown, Quote, AlignRight as AlignRight$1, AlignLeft as AlignLeft$1, AlignJustify, AlignCenter as AlignCenter$1, Plus, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, Info, CircleAlert, CircleCheck as CircleCheck$1, CircleIcon, ChevronDownIcon as ChevronDownIcon$1, File as File$1, ChevronUp, Edit, Pencil, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, WifiOff, TriangleAlert, RefreshCw, GitBranch, CircleHelp, Loader2, Lock, ArrowUpDown, GitFork, Globe, Copy, Folder as Folder$1, Clapperboard, LayoutGrid, List as List$1, CloudUpload, ArrowDownToLine, TextCursorInput, Images, AlertCircle, Image, Database, CheckCircle2, ImageOff, KeyRound, Cloud, LoaderCircle, ScrollText, RefreshCwOff, OctagonAlert, Menu, FilePlus, LogOut, FileStack, FolderOpen, History, Users, PanelLeft, Files, User, FileText, GitBranchIcon, Circle as Circle$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink, SquarePen, Trash2, House, Unlock } from "lucide-react";
25
24
  import { Slot } from "@radix-ui/react-slot";
26
25
  import { cva } from "class-variance-authority";
27
26
  import { Command as Command$2 } from "cmdk";
@@ -33,7 +32,7 @@ import { useHTMLInputCursorState, useComboboxInput } from "@udecode/plate-combob
33
32
  import { useComboboxContext, Combobox as Combobox$1, useComboboxStore, ComboboxProvider, Portal, ComboboxPopover, ComboboxItem } from "@ariakit/react";
34
33
  import { filterWords } from "@udecode/plate-combobox";
35
34
  import { toggleList, unwrapList, getListItemEntry } from "@udecode/plate-list";
36
- import { normalizeHeadingLevels, ALL_HEADING_LEVELS, ASYNC_POLLER_ERROR, canonicalPath, ERR_BRANCH_EXISTS, ERR_BRANCH_CONFLICT, isHeadingLevel, TinaSchema, addNamespaceToSchema, parseURL, ERR_HAS_REFERENCES, ERR_ALREADY_EXISTS, isValidRelativePath, RELATIVE_PATH_ALLOWED_CHARS_MESSAGE, resolveForm, RELATIVE_PATH_REGEX, normalizePath, ERR_NOT_INDEXED, validateSchema } from "@tinacms/schema-tools";
35
+ import { normalizeHeadingLevels, ALL_HEADING_LEVELS, MEDIA_MIME_TYPES, extensionOf, resolveMediaAccept, ASYNC_POLLER_ERROR, MEDIA_CATEGORIES, canonicalPath, ERR_BRANCH_EXISTS, ERR_BRANCH_CONFLICT, isHeadingLevel, TinaSchema, addNamespaceToSchema, parseURL, ERR_HAS_REFERENCES, ERR_ALREADY_EXISTS, isValidRelativePath, RELATIVE_PATH_ALLOWED_CHARS_MESSAGE, resolveForm, RELATIVE_PATH_REGEX, normalizePath, ERR_NOT_INDEXED, validateSchema } from "@tinacms/schema-tools";
37
36
  import { NAMER, resolveField } from "@tinacms/schema-tools";
38
37
  import { useBlockSelected, BlockSelectionPlugin } from "@udecode/plate-selection/react";
39
38
  import "@udecode/plate-dnd";
@@ -63,12 +62,12 @@ import { ExitBreakPlugin, SoftBreakPlugin } from "@udecode/plate-break/react";
63
62
  import { ResetNodePlugin } from "@udecode/plate-reset-node/react";
64
63
  import { TrailingBlockPlugin } from "@udecode/plate-trailing-block";
65
64
  import { BaseBlockquotePlugin } from "@udecode/plate-block-quote";
65
+ import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
66
66
  import { HeadingPlugin } from "@udecode/plate-heading/react";
67
67
  import { IndentListPlugin } from "@udecode/plate-indent-list/react";
68
68
  import { NodeIdPlugin } from "@udecode/plate-node-id";
69
69
  import { offset, flip, shift, limitShift, useVirtualFloating, getSelectionBoundingClientRect } from "@udecode/plate-floating";
70
70
  import { getLinkAttributes } from "@udecode/plate-link";
71
- import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
72
71
  import * as yup from "yup";
73
72
  import { format as format$3, formatDistanceToNow } from "date-fns";
74
73
  import { enUS } from "date-fns/locale";
@@ -1396,20 +1395,6 @@ function CodeBlockCombobox({
1396
1395
  )))))
1397
1396
  ));
1398
1397
  }
1399
- const MermaidElementWithRef = ({ config }) => {
1400
- const mermaidRef = useRef(null);
1401
- useEffect(() => {
1402
- if (mermaidRef.current) {
1403
- const renderMermaid = async () => {
1404
- await mermaid.run({
1405
- nodes: [mermaidRef.current.querySelector(".mermaid")]
1406
- });
1407
- };
1408
- renderMermaid();
1409
- }
1410
- }, [config]);
1411
- return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "border-border border-b pt-10" }, /* @__PURE__ */ React__default.createElement("div", { ref: mermaidRef }, /* @__PURE__ */ React__default.createElement("pre", { className: "mermaid not-tina-prose" }, config)));
1412
- };
1413
1398
  function ErrorMessage$1({ error }) {
1414
1399
  if (!error) return null;
1415
1400
  return /* @__PURE__ */ React__default.createElement(
@@ -1423,6 +1408,52 @@ function ErrorMessage$1({ error }) {
1423
1408
  /* @__PURE__ */ React__default.createElement("div", { className: "ml-3 flex-1" }, /* @__PURE__ */ React__default.createElement("pre", { className: "m-0 font-mono text-sm text-red-700 whitespace-pre-wrap break-words" }, error))
1424
1409
  );
1425
1410
  }
1411
+ const RENDERER_FAILED = "The diagram renderer did not load.";
1412
+ const MermaidElementWithRef = ({ config }) => {
1413
+ const mermaidRef = useRef(null);
1414
+ const [status, setStatus] = useState("loading");
1415
+ const [error, setError] = useState(null);
1416
+ useEffect(() => {
1417
+ let isCancelled = false;
1418
+ setStatus("loading");
1419
+ const render = async () => {
1420
+ var _a;
1421
+ try {
1422
+ const node = (_a = mermaidRef.current) == null ? void 0 : _a.querySelector(".mermaid");
1423
+ if (!node) throw new Error(RENDERER_FAILED);
1424
+ const { default: mermaid } = await import("mermaid");
1425
+ await mermaid.run({ nodes: [node] });
1426
+ if (!isCancelled) {
1427
+ setError(null);
1428
+ setStatus("ready");
1429
+ }
1430
+ } catch (cause) {
1431
+ if (isCancelled) {
1432
+ return;
1433
+ }
1434
+ if (cause instanceof Error) {
1435
+ setError(cause.message || RENDERER_FAILED);
1436
+ } else {
1437
+ setError(RENDERER_FAILED);
1438
+ }
1439
+ setStatus("failed");
1440
+ }
1441
+ };
1442
+ render();
1443
+ return () => {
1444
+ isCancelled = true;
1445
+ };
1446
+ }, [config]);
1447
+ return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "border-border border-b pt-10" }, /* @__PURE__ */ React__default.createElement("div", { className: "relative" }, /* @__PURE__ */ React__default.createElement(
1448
+ "div",
1449
+ {
1450
+ ref: mermaidRef,
1451
+ className: status === "loading" ? "opacity-0" : void 0
1452
+ },
1453
+ /* @__PURE__ */ React__default.createElement("pre", { className: "mermaid not-tina-prose" }, config)
1454
+ ), status === "loading" && /* @__PURE__ */ React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-muted-foreground" }, "Loading diagram…")), status === "failed" && /* @__PURE__ */ React__default.createElement(ErrorMessage$1, { error }));
1455
+ };
1456
+ const PARSE_FAILED = "An error occurred while parsing the diagram.";
1426
1457
  function codeLineToString(content) {
1427
1458
  return (content.children || []).map(
1428
1459
  (line) => (line.children || []).map((textNode) => textNode.text).join("")
@@ -1440,17 +1471,21 @@ const CodeBlockElement = withRef(
1440
1471
  let isCancelled = false;
1441
1472
  const validateMermaid = async () => {
1442
1473
  try {
1474
+ const { default: mermaid } = await import("mermaid");
1443
1475
  await mermaid.parse(
1444
1476
  codeLineToString(element)
1445
1477
  );
1446
1478
  if (!isCancelled) {
1447
1479
  setCodeBlockError(null);
1448
1480
  }
1449
- } catch (err) {
1450
- if (!isCancelled) {
1451
- setCodeBlockError(
1452
- String(err.message) || "An error occurred while parsing the diagram."
1453
- );
1481
+ } catch (cause) {
1482
+ if (isCancelled) {
1483
+ return;
1484
+ }
1485
+ if (cause instanceof Error) {
1486
+ setCodeBlockError(cause.message || PARSE_FAILED);
1487
+ } else {
1488
+ setCodeBlockError(PARSE_FAILED);
1454
1489
  }
1455
1490
  }
1456
1491
  };
@@ -2759,7 +2794,7 @@ const DropdownMenuTrigger$1 = DropdownMenuPrimitive.Trigger;
2759
2794
  const DropdownMenuGroup = DropdownMenuPrimitive.Group;
2760
2795
  const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
2761
2796
  const DropdownMenuSub = DropdownMenuPrimitive.Sub;
2762
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
2797
+ const DropdownMenuRadioGroup$1 = DropdownMenuPrimitive.RadioGroup;
2763
2798
  const DropdownMenuSubTrigger = withRef(({ children, className, inset, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2764
2799
  DropdownMenuPrimitive.SubTrigger,
2765
2800
  {
@@ -2818,7 +2853,7 @@ const DropdownMenuCheckboxItem = withRef(({ children, className, ...props }, ref
2818
2853
  /* @__PURE__ */ React__default.createElement("span", { className: "absolute left-2 flex size-3.5 items-center justify-center" }, /* @__PURE__ */ React__default.createElement(DropdownMenuPrimitive.ItemIndicator, null, /* @__PURE__ */ React__default.createElement(Icons.check, { className: "size-4" }))),
2819
2854
  children
2820
2855
  ));
2821
- const DropdownMenuRadioItem = withRef(({ children, className, hideIcon, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2856
+ const DropdownMenuRadioItem$1 = withRef(({ children, className, hideIcon, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2822
2857
  DropdownMenuPrimitive.RadioItem,
2823
2858
  {
2824
2859
  className: cn$1(
@@ -2842,7 +2877,7 @@ const dropdownMenuLabelVariants = cva(
2842
2877
  }
2843
2878
  }
2844
2879
  );
2845
- const DropdownMenuLabel = withVariants(
2880
+ const DropdownMenuLabel$1 = withVariants(
2846
2881
  DropdownMenuPrimitive.Label,
2847
2882
  dropdownMenuLabelVariants,
2848
2883
  ["inset"]
@@ -4166,6 +4201,17 @@ TinaField.propTypes = {
4166
4201
  Component: PropTypes.any.isRequired,
4167
4202
  children: PropTypes.any
4168
4203
  };
4204
+ const isErrorNamed = (error, name2) => error instanceof Error && error.name === name2;
4205
+ class SessionExpiredError extends Error {
4206
+ constructor() {
4207
+ super("Your session has ended. Please sign in again.");
4208
+ this.name = "SessionExpiredError";
4209
+ }
4210
+ }
4211
+ const isSessionExpiredError = (error) => error instanceof SessionExpiredError || isErrorNamed(error, "SessionExpiredError");
4212
+ const dispatchSessionExpired = (events) => {
4213
+ events.dispatch({ type: "cms:session-expired" });
4214
+ };
4169
4215
  const LoadingDots = ({
4170
4216
  dotSize = 8,
4171
4217
  color = "white"
@@ -4474,6 +4520,54 @@ function DropdownMenuItem({
4474
4520
  }
4475
4521
  );
4476
4522
  }
4523
+ function DropdownMenuRadioGroup({
4524
+ ...props
4525
+ }) {
4526
+ return /* @__PURE__ */ React.createElement(
4527
+ DropdownMenuPrimitive.RadioGroup,
4528
+ {
4529
+ "data-slot": "dropdown-menu-radio-group",
4530
+ ...props
4531
+ }
4532
+ );
4533
+ }
4534
+ function DropdownMenuRadioItem({
4535
+ className,
4536
+ children,
4537
+ ...props
4538
+ }) {
4539
+ return /* @__PURE__ */ React.createElement(
4540
+ DropdownMenuPrimitive.RadioItem,
4541
+ {
4542
+ "data-slot": "dropdown-menu-radio-item",
4543
+ className: cn(
4544
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
4545
+ className
4546
+ ),
4547
+ ...props
4548
+ },
4549
+ /* @__PURE__ */ React.createElement("span", { className: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center" }, /* @__PURE__ */ React.createElement(DropdownMenuPrimitive.ItemIndicator, null, /* @__PURE__ */ React.createElement(CircleIcon, { className: "size-2 fill-current" }))),
4550
+ children
4551
+ );
4552
+ }
4553
+ function DropdownMenuLabel({
4554
+ className,
4555
+ inset,
4556
+ ...props
4557
+ }) {
4558
+ return /* @__PURE__ */ React.createElement(
4559
+ DropdownMenuPrimitive.Label,
4560
+ {
4561
+ "data-slot": "dropdown-menu-label",
4562
+ "data-inset": inset,
4563
+ className: cn(
4564
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
4565
+ className
4566
+ ),
4567
+ ...props
4568
+ }
4569
+ );
4570
+ }
4477
4571
  function DropdownMenuSeparator({
4478
4572
  className,
4479
4573
  ...props
@@ -5557,12 +5651,38 @@ const dropzoneAcceptFromString = (str) => {
5557
5651
  ...(str || DEFAULT_MEDIA_UPLOAD_TYPES).split(",").map((x) => ({ [x]: [] }))
5558
5652
  );
5559
5653
  };
5560
- const isImage = (filename) => {
5561
- return /\.(gif|jpg|jpeg|tiff|png|svg|webp|avif)(\?.*)?$/i.test(filename);
5562
- };
5563
- const isVideo = (filename) => {
5564
- return /\.(mp4|webm|ogg|m4v|mov|avi|flv|mkv)(\?.*)?$/i.test(filename);
5654
+ const dropzoneAcceptFromExtensions = (extensions) => {
5655
+ if (!extensions.length) return void 0;
5656
+ const accept = {};
5657
+ for (const ext of extensions) {
5658
+ const mimeType = MEDIA_MIME_TYPES[ext];
5659
+ accept[mimeType] ?? (accept[mimeType] = []);
5660
+ accept[mimeType].push(`.${ext}`);
5661
+ }
5662
+ return accept;
5565
5663
  };
5664
+ const PREVIEWABLE_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
5665
+ "gif",
5666
+ "jpg",
5667
+ "jpeg",
5668
+ "tiff",
5669
+ "png",
5670
+ "svg",
5671
+ "webp",
5672
+ "avif"
5673
+ ]);
5674
+ const PREVIEWABLE_VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5675
+ "mp4",
5676
+ "webm",
5677
+ "ogg",
5678
+ "m4v",
5679
+ "mov",
5680
+ "avi",
5681
+ "flv",
5682
+ "mkv"
5683
+ ]);
5684
+ const isImage = (filename) => PREVIEWABLE_IMAGE_EXTENSIONS.has(extensionOf(filename ?? ""));
5685
+ const isVideo = (filename) => PREVIEWABLE_VIDEO_EXTENSIONS.has(extensionOf(filename ?? ""));
5566
5686
  const absoluteImgURL = (str) => {
5567
5687
  if (str.startsWith("http")) return str;
5568
5688
  return `${window.location.origin}${str}`;
@@ -5624,12 +5744,13 @@ const StyledImage = ({ src }) => {
5624
5744
  const StyledFile = ({ src }) => {
5625
5745
  return /* @__PURE__ */ React.createElement("div", { className: "max-w-full w-full flex-1 flex justify-start items-center gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "w-12 h-12 bg-white shadow border border-gray-100 rounded flex justify-center flex-none" }, /* @__PURE__ */ React.createElement(File$1, { className: "w-3/5 h-full text-gray-300" })), /* @__PURE__ */ React.createElement("span", { className: "text-base text-left flex-1 text-gray-500 w-full break-words truncate" }, src));
5626
5746
  };
5627
- const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading }, ref) => {
5747
+ const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading, accept }, ref) => {
5628
5748
  const cms = useCMS();
5629
5749
  const { getRootProps, getInputProps } = useDropzone$1({
5630
- accept: dropzoneAcceptFromString(
5631
- cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
5632
- ),
5750
+ // A field's `accept` overrides the global `media.accept`, so a file the
5751
+ // field would refuse is rejected here rather than uploaded and then
5752
+ // discarded by the insert guard.
5753
+ accept: dropzoneAcceptFromExtensions(resolveMediaAccept(accept)) ?? dropzoneAcceptFromString(cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES),
5633
5754
  onDrop,
5634
5755
  noClick: !!onClick
5635
5756
  });
@@ -7349,6 +7470,16 @@ const ImageField = wrapFieldsWithMeta(
7349
7470
  var _a, _b;
7350
7471
  if (media) {
7351
7472
  const item = Array.isArray(media) ? media[0] : media;
7473
+ const allowed = resolveMediaAccept(
7474
+ props.field.accept
7475
+ );
7476
+ const ext = extensionOf(item.filename) || extensionOf(item.src ?? "");
7477
+ if (allowed.length && !allowed.includes(ext)) {
7478
+ cms.alerts.error(
7479
+ `${item.filename} is not an accepted file type. This field accepts ${allowed.join(", ")}.`
7480
+ );
7481
+ return;
7482
+ }
7352
7483
  const parsedValue = typeof ((_b = (_a = cms == null ? void 0 : cms.media) == null ? void 0 : _a.store) == null ? void 0 : _b.parse) === "function" ? cms.media.store.parse(item) : item.src || item;
7353
7484
  props.input.onChange(parsedValue);
7354
7485
  }
@@ -7361,12 +7492,14 @@ const ImageField = wrapFieldsWithMeta(
7361
7492
  value,
7362
7493
  src,
7363
7494
  loading: isImgUploading,
7495
+ accept: props.field.accept,
7364
7496
  onClick: () => {
7365
7497
  const directory = uploadDir(props.form.getState().values);
7366
7498
  cms.media.open({
7367
7499
  allowDelete: true,
7368
7500
  directory,
7369
- onSelect: onChange
7501
+ onSelect: onChange,
7502
+ accept: props.field.accept
7370
7503
  });
7371
7504
  },
7372
7505
  onDrop: async ([file], fileRejections) => {
@@ -9938,9 +10071,21 @@ let Alerts$1 = class Alerts {
9938
10071
  this.add(level, message, timeout);
9939
10072
  }
9940
10073
  });
10074
+ /**
10075
+ * While suppressed (the window between a session expiring and the user
10076
+ * signing back in), new alerts are dropped so nothing paints over the
10077
+ * login modal. Alerts already showing are left alone.
10078
+ */
10079
+ __publicField(this, "suppressed", false);
9941
10080
  this.events = events;
9942
10081
  this.map = map;
9943
10082
  this.events.subscribe("*", this.mapEventToAlert);
10083
+ this.events.subscribe("cms:session-expired", () => {
10084
+ this.suppressed = true;
10085
+ });
10086
+ this.events.subscribe("cms:login", () => {
10087
+ this.suppressed = false;
10088
+ });
9944
10089
  }
9945
10090
  setMap(eventsToAlerts) {
9946
10091
  this.map = {
@@ -9949,6 +10094,10 @@ let Alerts$1 = class Alerts {
9949
10094
  };
9950
10095
  }
9951
10096
  add(level, message, timeout = 4e3) {
10097
+ if (this.suppressed) {
10098
+ return () => {
10099
+ };
10100
+ }
9952
10101
  const alert = {
9953
10102
  level,
9954
10103
  message,
@@ -9989,219 +10138,6 @@ let Alerts$1 = class Alerts {
9989
10138
  return this.add("error", message, timeout);
9990
10139
  }
9991
10140
  };
9992
- class PluginTypeManager {
9993
- constructor(events) {
9994
- /**
9995
- * @ignore
9996
- */
9997
- __publicField(this, "plugins", {});
9998
- this.events = events;
9999
- }
10000
- /**
10001
- * Gets the [[PluginType|collection of plugins]] for the given type.
10002
- *
10003
- * #### Example: Basic Usage
10004
- *
10005
- * ```ts
10006
- * const colorPlugins = cms.plugins.get("color")
10007
- * ```
10008
- *
10009
- * #### Example: Advanced Types
10010
- *
10011
- * A type param can be added to specify the kind of Plugin
10012
- * that is being listed.
10013
- *
10014
- * ```ts
10015
- * const colorPlugins = cms.plugins.get<ColorPlugin>("color")
10016
- * ```
10017
- *
10018
- * @param type The type of plugins to be retrieved
10019
- * @typeparam P A subclass of Plugin. Optional.
10020
- */
10021
- getType(type2) {
10022
- return this.plugins[type2] = this.plugins[type2] || new PluginType(type2, this.events);
10023
- }
10024
- /**
10025
- * An alias to [[get]]
10026
- *
10027
- * ### !DEPRECATED!
10028
- *
10029
- * This name is unnecessarily verbose and weird.
10030
- */
10031
- findOrCreateMap(type2) {
10032
- return this.getType(type2);
10033
- }
10034
- /**
10035
- * Adds a Plugin to the CMS.
10036
- *
10037
- * #### Example: Basic Usage
10038
- *
10039
- * ```js
10040
- * cms.plugins.add({ __type: "color", name: "red" })
10041
- * ```
10042
- *
10043
- * #### Example: Advanced Types
10044
- *
10045
- * ```ts
10046
- * interface ColorPlugin extends Plugin {
10047
- * __type: "color"
10048
- * hex: string
10049
- * rgb: string
10050
- * }
10051
- *
10052
- * cms.plugins.add<ColorPlugin>({
10053
- * __type: "color",
10054
- * name: "red",
10055
- * hex: "#FF0000",
10056
- * rgb: "RGBA(255, 0, 0, 1)"
10057
- * })
10058
- * ```
10059
- *
10060
- * @typeparam P A subclass of Plugin. Optional.
10061
- * @param plugin
10062
- * @todo Consider returning the plugin which was just added.
10063
- */
10064
- add(plugin) {
10065
- this.findOrCreateMap(plugin.__type).add(plugin);
10066
- }
10067
- /**
10068
- * Removes the given plugin from the CMS.
10069
- *
10070
- * #### Example
10071
- *
10072
- * In this example a plugin is added to the CMS and removed
10073
- * 5 seconds later.
10074
- *
10075
- * ```ts
10076
- * const redPlugin = {
10077
- * __type: "color",
10078
- * name: "red",
10079
- * hex: "#FF0000",
10080
- * rgb: "RGBA(255, 0, 0, 1)"
10081
- * }
10082
- *
10083
- * cms.plugins.add(redPlugin)
10084
- *
10085
- * setTimeout(() => {
10086
- * cms.plugins.remove(redPlugin)
10087
- * }, 5000)
10088
- * ```
10089
- *
10090
- * @typeparam P A subclass of Plugin. Optional.
10091
- * @param plugin The plugin to be removed from the CMS.
10092
- */
10093
- remove(plugin) {
10094
- this.findOrCreateMap(plugin.__type).remove(plugin);
10095
- }
10096
- /**
10097
- * Returns a list of all the plugins of the given type.
10098
- *
10099
- * #### Example: Basic Usage
10100
- *
10101
- * ```ts
10102
- * cms.plugins.all("color").forEach(color => {
10103
- * console.log(color.name)
10104
- * })
10105
- * ```
10106
- *
10107
- * #### Example: Advanced Types
10108
- *
10109
- * A generic can be added to specify the type of plugins
10110
- * that are being retrieved.
10111
- *
10112
- * ```ts
10113
- * cms.plugins.all<ColorPlugin>("color").forEach(color => {
10114
- * console.log(color.name, color.hex)
10115
- * })
10116
- * ```
10117
- *
10118
- * @typeparam P A subclass of Plugin. Optional.
10119
- * @param type The name of the plugin
10120
- * @returns An array of all plugins of the given type.
10121
- */
10122
- all(type2) {
10123
- return this.findOrCreateMap(type2).all();
10124
- }
10125
- }
10126
- class PluginType {
10127
- /**
10128
- *
10129
- * @param __type The `__type` of plugin being managed.
10130
- */
10131
- constructor(__type, events) {
10132
- /**
10133
- * @ignore
10134
- */
10135
- __publicField(this, "__plugins", {});
10136
- this.__type = __type;
10137
- this.events = events;
10138
- }
10139
- /**
10140
- * Adds a new plugin to the collection.
10141
- *
10142
- * ### Example
10143
- *
10144
- * ```ts
10145
- * interface ColorPlugin extends Plugin {
10146
- * hex: string
10147
- * }
10148
- *
10149
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10150
- *
10151
- * colorPlugins.add({ name: "red", hex: "#f00" })
10152
- * ```
10153
- *
10154
- * @param plugin A new plugin. The `__type` is optional and will be added if it's missing.
10155
- */
10156
- add(plugin) {
10157
- const p = plugin;
10158
- if (!p.__type) {
10159
- p.__type = this.__type;
10160
- }
10161
- this.__plugins[p.name] = p;
10162
- this.events.dispatch({ type: `plugin:add:${this.__type}` });
10163
- }
10164
- all() {
10165
- return Object.keys(this.__plugins).map((name2) => this.__plugins[name2]);
10166
- }
10167
- /**
10168
- *
10169
- * Looks up a plugin by it's `name`.
10170
- *
10171
- * ### Example
10172
- *
10173
- * ```ts
10174
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10175
- *
10176
- * colorPlugins.add({ name: "red", hex: "#f00" })
10177
- *
10178
- * colorPlugins.find("red") // { __type: "color", name: "red", hex: "#f00" }
10179
- * colorPlugin.find("large") // undefined
10180
- * ```
10181
- *
10182
- * @param name The `name` of the plugin to be retrieved.
10183
- */
10184
- find(name2) {
10185
- return this.__plugins[name2];
10186
- }
10187
- /**
10188
- * Pass this function a plugin or the `name` of a plugin to have
10189
- * it be removed from the CMS.
10190
- *
10191
- * @param pluginOrName The `name` of a plugin, or the plugin itself.
10192
- * @returns The plugin that was removed, or `undefined` if it was not found.
10193
- */
10194
- remove(pluginOrName) {
10195
- const name2 = typeof pluginOrName === "string" ? pluginOrName : pluginOrName.name;
10196
- const plugin = this.__plugins[name2];
10197
- delete this.__plugins[name2];
10198
- this.events.dispatch({ type: `plugin:remove:${this.__type}` });
10199
- return plugin;
10200
- }
10201
- subscribe(cb) {
10202
- return this.events.subscribe(`plugin:*:${this.__type}`, cb);
10203
- }
10204
- }
10205
10141
  class EventBus {
10206
10142
  constructor() {
10207
10143
  __publicField(this, "listeners", /* @__PURE__ */ new Set());
@@ -10280,6 +10216,19 @@ class Listener {
10280
10216
  return !ignoresEvent;
10281
10217
  }
10282
10218
  }
10219
+ class Flags {
10220
+ constructor(events) {
10221
+ __publicField(this, "_flags", /* @__PURE__ */ new Map());
10222
+ this.events = events;
10223
+ }
10224
+ get(key) {
10225
+ return this._flags.get(key);
10226
+ }
10227
+ set(key, value) {
10228
+ this._flags.set(key, value);
10229
+ this.events.dispatch({ type: "flag:set", key, value });
10230
+ }
10231
+ }
10283
10232
  const EDITORIAL_WORKFLOW_STATUS = {
10284
10233
  QUEUED: "queued",
10285
10234
  PROCESSING: "processing",
@@ -10367,6 +10316,26 @@ const MEDIA_WORKFLOW_STATUS_TO_STEP = {
10367
10316
  [EDITORIAL_WORKFLOW_STATUS.COMPLETE]: MEDIA_WORKFLOW_STEP.COMPLETE
10368
10317
  };
10369
10318
  const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
10319
+ const CANONICAL_THUMBNAIL_SIZES = [
10320
+ { w: 75, h: 75 },
10321
+ { w: 400, h: 400 },
10322
+ { w: 1e3, h: 1e3 }
10323
+ ];
10324
+ const RENAME_ERROR_CODES = [
10325
+ "NOT_FOUND",
10326
+ "NAME_COLLISION",
10327
+ "INVALID_FILENAME",
10328
+ "INVALID_PATH",
10329
+ "UNAUTHORIZED",
10330
+ "UNSUPPORTED",
10331
+ "BACKEND_FAILURE"
10332
+ ];
10333
+ class MediaRenameCancelled extends Error {
10334
+ constructor() {
10335
+ super("Media rename cancelled.");
10336
+ __publicField(this, "ERR_TYPE", "MediaRenameCancelled");
10337
+ }
10338
+ }
10370
10339
  class DummyMediaStore {
10371
10340
  constructor() {
10372
10341
  __publicField(this, "accept", "*");
@@ -10408,10 +10377,9 @@ class TinaMediaStore {
10408
10377
  __publicField(this, "workflowBranchOverride");
10409
10378
  __publicField(this, "mediaWorkflowInProgress", false);
10410
10379
  /**
10411
- * Renaming is only implemented against the local dev server. This is an
10412
- * instance property rather than a prototype method on purpose: the media
10380
+ * An instance property rather than a prototype method on purpose: the media
10413
10381
  * manager decides whether to offer the action by checking whether the store
10414
- * defines `rename`, so a cloud-backed instance must not carry one — a
10382
+ * defines `rename`, so an instance that cannot rename must not carry one — a
10415
10383
  * prototype method would advertise support everywhere and surface an action
10416
10384
  * that always fails.
10417
10385
  */
@@ -10429,8 +10397,8 @@ class TinaMediaStore {
10429
10397
  this.isStatic = true;
10430
10398
  this.staticMedia = staticMedia;
10431
10399
  }
10432
- if (!this.isStatic && ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode)) {
10433
- this.rename = (from, to2) => this.rename_local(from, to2);
10400
+ if (!this.isStatic && !this.isUnsupportedSelfHostedRepoMedia()) {
10401
+ this.rename = ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode) ? (from, to2) => this.rename_local(from, to2) : (from, to2) => this.rename_cloud(from, to2);
10434
10402
  }
10435
10403
  }
10436
10404
  /**
@@ -10476,27 +10444,29 @@ class TinaMediaStore {
10476
10444
  return await this.api.authProvider.isAuthenticated();
10477
10445
  }
10478
10446
  /**
10479
- * Returns the workflow branch override or current branch as a single-encoded
10480
- * query-param value, or an empty string when no branch is set.
10481
- *
10482
- * `this.api.branch` is already URL-encoded by `Client.setBranch()`, so we
10483
- * decode then re-encode here to defend against double-encoding when this
10484
- * value is concatenated into a URL.
10485
- *
10486
- * `Client.setBranch()` runs the constructor's `options.branch` through
10487
- * `encodeURIComponent` without a guard, so an unset `options.branch`
10488
- * lands here as the literal string `"undefined"`. We treat that and the
10489
- * empty case as no-branch so we don't send `?branch=undefined` to the
10490
- * assets-api (which would route the call to a non-existent staging path).
10447
+ * The v2 cloud endpoint and the local dev server both filter by `ext` before
10448
+ * paginating. `staticMedia` is a build-time snapshot with no such pass, and
10449
+ * filtering a page client-side would leave a near-empty grid while matches
10450
+ * sit further down so a static store reports no support and the control
10451
+ * stays hidden, the same way `searchable` is gated.
10491
10452
  */
10492
- encodedBranchParam() {
10493
- if (this.workflowBranchOverride) {
10494
- return encodeURIComponent(this.workflowBranchOverride);
10495
- }
10453
+ get extensionFilterable() {
10454
+ return !this.isStatic;
10455
+ }
10456
+ /**
10457
+ * `Client.setBranch()` encodes an unset branch to the literal `"undefined"`,
10458
+ * which the assets API would read as a real (non-existent) staging path.
10459
+ */
10460
+ currentBranch() {
10461
+ if (this.workflowBranchOverride) return this.workflowBranchOverride;
10496
10462
  if (!this.api.branch) return "";
10497
10463
  const decoded = decodeURIComponent(this.api.branch);
10498
- if (!decoded || decoded === "undefined") return "";
10499
- return encodeURIComponent(decoded);
10464
+ return decoded === "undefined" ? "" : decoded;
10465
+ }
10466
+ /** `this.api.branch` arrives encoded, so encode the decoded name once here. */
10467
+ encodedBranchParam() {
10468
+ const branch = this.currentBranch();
10469
+ return branch ? encodeURIComponent(branch) : "";
10500
10470
  }
10501
10471
  shortStableHash(input) {
10502
10472
  let hash = 2166136261;
@@ -10534,7 +10504,7 @@ class TinaMediaStore {
10534
10504
  const encodedBranch = this.encodedBranchParam();
10535
10505
  return encodedBranch ? `?branch=${encodedBranch}` : "";
10536
10506
  }
10537
- requestMediaBranchChoice(branchName, baseBranch, opType, repoPath) {
10507
+ requestMediaBranchChoice(branchName, baseBranch, request) {
10538
10508
  if (!this.cms.events.hasExplicitListenerFor("media:workflow:confirm-branch")) {
10539
10509
  throw new Error(
10540
10510
  "Cannot start a media editorial workflow: no branch prompt is mounted. Ensure <MediaWorkflowOverlay /> is rendered (TinaCloudProvider mounts it automatically)."
@@ -10545,12 +10515,13 @@ class TinaMediaStore {
10545
10515
  type: "media:workflow:confirm-branch",
10546
10516
  branchName,
10547
10517
  baseBranch,
10518
+ // A rename on a protected branch can only go through the workflow.
10519
+ allowSaveToProtectedBranch: request.opType !== "rename",
10548
10520
  onConfirm: async (selectedBranchName) => {
10549
10521
  const context = await this.prepareMediaBranch(
10550
10522
  selectedBranchName,
10551
10523
  baseBranch,
10552
- opType,
10553
- repoPath
10524
+ request
10554
10525
  );
10555
10526
  resolve({
10556
10527
  kind: "workflow",
@@ -10562,7 +10533,7 @@ class TinaMediaStore {
10562
10533
  });
10563
10534
  });
10564
10535
  }
10565
- async prepareMediaBranch(branchName, baseBranch, opType, repoPath) {
10536
+ async prepareMediaBranch(branchName, baseBranch, request) {
10566
10537
  if (this.mediaWorkflowInProgress) {
10567
10538
  throw new Error("A media workflow is already in progress.");
10568
10539
  }
@@ -10577,8 +10548,9 @@ class TinaMediaStore {
10577
10548
  branchName,
10578
10549
  baseBranch,
10579
10550
  prTitle: getEditorialWorkflowPrTitle(branchName),
10580
- operation: opType,
10581
- repoPath
10551
+ operation: request.opType,
10552
+ repoPath: request.repoPath,
10553
+ targetRepoPath: request.targetRepoPath
10582
10554
  });
10583
10555
  const branchContext = {
10584
10556
  branchName: workflow.branchName || branchName,
@@ -10654,12 +10626,10 @@ class TinaMediaStore {
10654
10626
  const branchName = `media-${opType}-${mediaSlug}`;
10655
10627
  const repoFilename = opType === "upload" && filename ? sanitizeFilename(filename) : filename;
10656
10628
  const repoPath = this.joinMediaPath(directory, repoFilename);
10657
- return this.requestMediaBranchChoice(
10658
- branchName,
10659
- baseBranch,
10629
+ return this.requestMediaBranchChoice(branchName, baseBranch, {
10660
10630
  opType,
10661
10631
  repoPath
10662
- );
10632
+ });
10663
10633
  }
10664
10634
  async waitForRequestStatus(requestId, timeoutMessage) {
10665
10635
  const startTime = Date.now();
@@ -10923,6 +10893,160 @@ class TinaMediaStore {
10923
10893
  lastSlash === -1 ? to2 : to2.slice(lastSlash + 1)
10924
10894
  );
10925
10895
  }
10896
+ /**
10897
+ * `from`/`to` are media-root-relative `directory/filename` paths. `to` is
10898
+ * already sanitised by `previewRename` and is passed through untouched.
10899
+ */
10900
+ async rename_cloud(from, to2) {
10901
+ this.setup();
10902
+ if (!await this.isAuthenticated()) {
10903
+ throw new MediaRenameError({
10904
+ code: "UNAUTHORIZED",
10905
+ message: "You don't have permission to rename this file."
10906
+ });
10907
+ }
10908
+ const decision = await this.prepareRenameBranch(from, to2);
10909
+ if (decision.kind === "cancelled") {
10910
+ throw new MediaRenameCancelled();
10911
+ }
10912
+ if (decision.kind === "workflow") {
10913
+ return this.renameCloudViaWorkflow(from, to2, decision.context);
10914
+ }
10915
+ return this.renameCloudDirect(from, to2);
10916
+ }
10917
+ /** `currentBranch()` is the workflow branch while one is active. */
10918
+ async postCloudRename(from, to2) {
10919
+ const branch = this.currentBranch() || void 0;
10920
+ const res = await this.api.authProvider.fetchWithToken(
10921
+ `${this.url}/rename`,
10922
+ {
10923
+ method: "POST",
10924
+ headers: { "Content-Type": "application/json" },
10925
+ body: JSON.stringify({ from, to: to2, branch })
10926
+ }
10927
+ );
10928
+ const body = await res.json().catch(() => null);
10929
+ if (res.status !== 200 || (body == null ? void 0 : body.success) !== true) {
10930
+ throw this.toCloudRenameError(res.status, body);
10931
+ }
10932
+ return body;
10933
+ }
10934
+ async renameCloudDirect(from, to2) {
10935
+ const result = await this.postCloudRename(from, to2);
10936
+ if (result.requestId) {
10937
+ try {
10938
+ await this.waitForRequestStatus(
10939
+ result.requestId,
10940
+ "Time out waiting for rename to complete"
10941
+ );
10942
+ } catch (error) {
10943
+ const detail = error instanceof Error && error.message ? `${error.message}. ` : "Failed to confirm the rename. ";
10944
+ throw new MediaRenameError({
10945
+ code: "BACKEND_FAILURE",
10946
+ message: `${detail}The file may still have been renamed — refresh the media library to check.`
10947
+ });
10948
+ }
10949
+ }
10950
+ return this.resolveRenamedMedia(result.path || to2, result.src);
10951
+ }
10952
+ /**
10953
+ * Completion is the workflow status, not the rename request status: only the
10954
+ * former waits for the branch, the media index and the pull request.
10955
+ */
10956
+ async renameCloudViaWorkflow(from, to2, branchContext) {
10957
+ let renamed;
10958
+ try {
10959
+ const result = await this.postCloudRename(from, to2);
10960
+ await this.finalizeMediaWorkflow(branchContext, async () => {
10961
+ renamed = await this.resolveRenamedMedia(result.path || to2, result.src);
10962
+ });
10963
+ } catch (err) {
10964
+ this.resetWorkflowState();
10965
+ this.cms.events.dispatch({ type: "media:workflow:finish" });
10966
+ throw err;
10967
+ }
10968
+ if (!renamed) {
10969
+ throw new MediaRenameError({
10970
+ code: "BACKEND_FAILURE",
10971
+ message: "The rename was staged but the branch workflow did not complete. Check the branch in TinaCloud before trying again."
10972
+ });
10973
+ }
10974
+ return renamed;
10975
+ }
10976
+ async prepareRenameBranch(from, to2) {
10977
+ if (!this.api.usingProtectedBranch()) return { kind: "direct" };
10978
+ const baseBranch = decodeURIComponent(this.api.branch || "");
10979
+ const { directory, filename } = this.splitMediaPath(from);
10980
+ const branchName = `media-rename-${this.branchSlugForMediaPath(
10981
+ directory,
10982
+ filename
10983
+ )}`;
10984
+ return this.requestMediaBranchChoice(branchName, baseBranch, {
10985
+ opType: "rename",
10986
+ repoPath: from,
10987
+ targetRepoPath: to2
10988
+ });
10989
+ }
10990
+ splitMediaPath(path) {
10991
+ const lastSlash = path.lastIndexOf("/");
10992
+ return lastSlash === -1 ? { directory: "", filename: path } : {
10993
+ directory: path.slice(0, lastSlash),
10994
+ filename: path.slice(lastSlash + 1)
10995
+ };
10996
+ }
10997
+ /**
10998
+ * The listing is the source of truth for `src` (staging path, per-stage CDN
10999
+ * host) and thumbnails; the rename response is the fallback when the entry
11000
+ * isn't on the first page.
11001
+ */
11002
+ async resolveRenamedMedia(targetPath, srcFromResponse) {
11003
+ const { directory, filename } = this.splitMediaPath(targetPath);
11004
+ try {
11005
+ const listed = await this.list({
11006
+ directory,
11007
+ limit: 100,
11008
+ thumbnailSizes: CANONICAL_THUMBNAIL_SIZES
11009
+ });
11010
+ const entry = listed.items.find(
11011
+ (item) => item.type === "file" && item.filename === filename
11012
+ );
11013
+ if (entry) return entry;
11014
+ } catch (error) {
11015
+ console.error("Failed to fetch the canonical media entry:", error);
11016
+ }
11017
+ const src = srcFromResponse || "";
11018
+ return {
11019
+ type: "file",
11020
+ id: filename,
11021
+ filename,
11022
+ directory,
11023
+ src,
11024
+ thumbnails: CANONICAL_THUMBNAIL_SIZES.reduce(
11025
+ (acc, size) => {
11026
+ acc[`${size.w}x${size.h}`] = this.genThumbnail(src, size);
11027
+ return acc;
11028
+ },
11029
+ {}
11030
+ )
11031
+ };
11032
+ }
11033
+ toCloudRenameError(status, body) {
11034
+ const message = typeof (body == null ? void 0 : body.message) === "string" && body.message ? body.message : "Failed to rename the file.";
11035
+ if (RENAME_ERROR_CODES.includes(body == null ? void 0 : body.code)) {
11036
+ return new MediaRenameError({ code: body.code, message });
11037
+ }
11038
+ let code;
11039
+ if (status === 401 || status === 403) {
11040
+ code = "UNAUTHORIZED";
11041
+ } else if (status === 404) {
11042
+ code = "NOT_FOUND";
11043
+ } else if (status === 409) {
11044
+ code = "NAME_COLLISION";
11045
+ } else {
11046
+ code = "BACKEND_FAILURE";
11047
+ }
11048
+ return new MediaRenameError({ code, message });
11049
+ }
10926
11050
  toRenameError(status, body) {
10927
11051
  if (body == null ? void 0 : body.code) {
10928
11052
  return new MediaRenameError({
@@ -10950,6 +11074,7 @@ class TinaMediaStore {
10950
11074
  return !this.isLocal ? `${src}?fit=crop&max-w=${dimensions.w}&max-h=${dimensions.h}` : src;
10951
11075
  }
10952
11076
  async list(options) {
11077
+ var _a, _b;
10953
11078
  this.setup();
10954
11079
  if (this.staticMedia) {
10955
11080
  const offset2 = options.offset || 0;
@@ -10981,13 +11106,16 @@ class TinaMediaStore {
10981
11106
  nextOffset: hasMore2 ? Number(offset2) + 20 : null
10982
11107
  };
10983
11108
  }
10984
- return { items: media, nextOffset: hasMore ? Number(offset2) + 20 : null };
11109
+ return {
11110
+ items: media,
11111
+ nextOffset: hasMore ? Number(offset2) + 20 : null
11112
+ };
10985
11113
  }
10986
11114
  let res;
10987
11115
  if (!this.isLocal) {
10988
11116
  const encodedBranch = this.encodedBranchParam();
10989
11117
  res = await this.api.authProvider.fetchWithToken(
10990
- `${this.listUrl}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${encodedBranch ? `&branch=${encodedBranch}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}`
11118
+ `${this.listUrl}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${encodedBranch ? `&branch=${encodedBranch}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}${((_a = options.ext) == null ? void 0 : _a.length) ? `&ext=${encodeURIComponent(options.ext.join(","))}` : ""}`
10991
11119
  );
10992
11120
  if (res.status == 401) {
10993
11121
  throw E_UNAUTHORIZED;
@@ -10997,7 +11125,7 @@ class TinaMediaStore {
10997
11125
  }
10998
11126
  } else {
10999
11127
  res = await this.fetchFunction(
11000
- `${this.url}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}`
11128
+ `${this.url}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}${((_b = options.ext) == null ? void 0 : _b.length) ? `&ext=${encodeURIComponent(options.ext.join(","))}` : ""}`
11001
11129
  );
11002
11130
  if (res.status == 404) {
11003
11131
  throw E_BAD_ROUTE;
@@ -11242,17 +11370,217 @@ const E_SELF_HOSTED_MEDIA = new MediaListError({
11242
11370
  message: "Self-hosted TinaCMS can't serve media from your repo through TinaCloud. Configure an external media store (e.g. S3, Cloudinary, DigitalOcean Spaces, or Azure) with media.loadCustomStore, or add media to your repo manually.",
11243
11371
  docsLink: "https://tina.io/docs/r/backend-media-handler/"
11244
11372
  });
11245
- class Flags {
11373
+ class PluginTypeManager {
11246
11374
  constructor(events) {
11247
- __publicField(this, "_flags", /* @__PURE__ */ new Map());
11375
+ /**
11376
+ * @ignore
11377
+ */
11378
+ __publicField(this, "plugins", {});
11248
11379
  this.events = events;
11249
11380
  }
11250
- get(key) {
11251
- return this._flags.get(key);
11381
+ /**
11382
+ * Gets the [[PluginType|collection of plugins]] for the given type.
11383
+ *
11384
+ * #### Example: Basic Usage
11385
+ *
11386
+ * ```ts
11387
+ * const colorPlugins = cms.plugins.get("color")
11388
+ * ```
11389
+ *
11390
+ * #### Example: Advanced Types
11391
+ *
11392
+ * A type param can be added to specify the kind of Plugin
11393
+ * that is being listed.
11394
+ *
11395
+ * ```ts
11396
+ * const colorPlugins = cms.plugins.get<ColorPlugin>("color")
11397
+ * ```
11398
+ *
11399
+ * @param type The type of plugins to be retrieved
11400
+ * @typeparam P A subclass of Plugin. Optional.
11401
+ */
11402
+ getType(type2) {
11403
+ return this.plugins[type2] = this.plugins[type2] || new PluginType(type2, this.events);
11252
11404
  }
11253
- set(key, value) {
11254
- this._flags.set(key, value);
11255
- this.events.dispatch({ type: "flag:set", key, value });
11405
+ /**
11406
+ * An alias to [[get]]
11407
+ *
11408
+ * ### !DEPRECATED!
11409
+ *
11410
+ * This name is unnecessarily verbose and weird.
11411
+ */
11412
+ findOrCreateMap(type2) {
11413
+ return this.getType(type2);
11414
+ }
11415
+ /**
11416
+ * Adds a Plugin to the CMS.
11417
+ *
11418
+ * #### Example: Basic Usage
11419
+ *
11420
+ * ```js
11421
+ * cms.plugins.add({ __type: "color", name: "red" })
11422
+ * ```
11423
+ *
11424
+ * #### Example: Advanced Types
11425
+ *
11426
+ * ```ts
11427
+ * interface ColorPlugin extends Plugin {
11428
+ * __type: "color"
11429
+ * hex: string
11430
+ * rgb: string
11431
+ * }
11432
+ *
11433
+ * cms.plugins.add<ColorPlugin>({
11434
+ * __type: "color",
11435
+ * name: "red",
11436
+ * hex: "#FF0000",
11437
+ * rgb: "RGBA(255, 0, 0, 1)"
11438
+ * })
11439
+ * ```
11440
+ *
11441
+ * @typeparam P A subclass of Plugin. Optional.
11442
+ * @param plugin
11443
+ * @todo Consider returning the plugin which was just added.
11444
+ */
11445
+ add(plugin) {
11446
+ this.findOrCreateMap(plugin.__type).add(plugin);
11447
+ }
11448
+ /**
11449
+ * Removes the given plugin from the CMS.
11450
+ *
11451
+ * #### Example
11452
+ *
11453
+ * In this example a plugin is added to the CMS and removed
11454
+ * 5 seconds later.
11455
+ *
11456
+ * ```ts
11457
+ * const redPlugin = {
11458
+ * __type: "color",
11459
+ * name: "red",
11460
+ * hex: "#FF0000",
11461
+ * rgb: "RGBA(255, 0, 0, 1)"
11462
+ * }
11463
+ *
11464
+ * cms.plugins.add(redPlugin)
11465
+ *
11466
+ * setTimeout(() => {
11467
+ * cms.plugins.remove(redPlugin)
11468
+ * }, 5000)
11469
+ * ```
11470
+ *
11471
+ * @typeparam P A subclass of Plugin. Optional.
11472
+ * @param plugin The plugin to be removed from the CMS.
11473
+ */
11474
+ remove(plugin) {
11475
+ this.findOrCreateMap(plugin.__type).remove(plugin);
11476
+ }
11477
+ /**
11478
+ * Returns a list of all the plugins of the given type.
11479
+ *
11480
+ * #### Example: Basic Usage
11481
+ *
11482
+ * ```ts
11483
+ * cms.plugins.all("color").forEach(color => {
11484
+ * console.log(color.name)
11485
+ * })
11486
+ * ```
11487
+ *
11488
+ * #### Example: Advanced Types
11489
+ *
11490
+ * A generic can be added to specify the type of plugins
11491
+ * that are being retrieved.
11492
+ *
11493
+ * ```ts
11494
+ * cms.plugins.all<ColorPlugin>("color").forEach(color => {
11495
+ * console.log(color.name, color.hex)
11496
+ * })
11497
+ * ```
11498
+ *
11499
+ * @typeparam P A subclass of Plugin. Optional.
11500
+ * @param type The name of the plugin
11501
+ * @returns An array of all plugins of the given type.
11502
+ */
11503
+ all(type2) {
11504
+ return this.findOrCreateMap(type2).all();
11505
+ }
11506
+ }
11507
+ class PluginType {
11508
+ /**
11509
+ *
11510
+ * @param __type The `__type` of plugin being managed.
11511
+ */
11512
+ constructor(__type, events) {
11513
+ /**
11514
+ * @ignore
11515
+ */
11516
+ __publicField(this, "__plugins", {});
11517
+ this.__type = __type;
11518
+ this.events = events;
11519
+ }
11520
+ /**
11521
+ * Adds a new plugin to the collection.
11522
+ *
11523
+ * ### Example
11524
+ *
11525
+ * ```ts
11526
+ * interface ColorPlugin extends Plugin {
11527
+ * hex: string
11528
+ * }
11529
+ *
11530
+ * const colorPlugins = new PluginType<ColorPlugin>("color")
11531
+ *
11532
+ * colorPlugins.add({ name: "red", hex: "#f00" })
11533
+ * ```
11534
+ *
11535
+ * @param plugin A new plugin. The `__type` is optional and will be added if it's missing.
11536
+ */
11537
+ add(plugin) {
11538
+ const p = plugin;
11539
+ if (!p.__type) {
11540
+ p.__type = this.__type;
11541
+ }
11542
+ this.__plugins[p.name] = p;
11543
+ this.events.dispatch({ type: `plugin:add:${this.__type}` });
11544
+ }
11545
+ all() {
11546
+ return Object.keys(this.__plugins).map((name2) => this.__plugins[name2]);
11547
+ }
11548
+ /**
11549
+ *
11550
+ * Looks up a plugin by it's `name`.
11551
+ *
11552
+ * ### Example
11553
+ *
11554
+ * ```ts
11555
+ * const colorPlugins = new PluginType<ColorPlugin>("color")
11556
+ *
11557
+ * colorPlugins.add({ name: "red", hex: "#f00" })
11558
+ *
11559
+ * colorPlugins.find("red") // { __type: "color", name: "red", hex: "#f00" }
11560
+ * colorPlugin.find("large") // undefined
11561
+ * ```
11562
+ *
11563
+ * @param name The `name` of the plugin to be retrieved.
11564
+ */
11565
+ find(name2) {
11566
+ return this.__plugins[name2];
11567
+ }
11568
+ /**
11569
+ * Pass this function a plugin or the `name` of a plugin to have
11570
+ * it be removed from the CMS.
11571
+ *
11572
+ * @param pluginOrName The `name` of a plugin, or the plugin itself.
11573
+ * @returns The plugin that was removed, or `undefined` if it was not found.
11574
+ */
11575
+ remove(pluginOrName) {
11576
+ const name2 = typeof pluginOrName === "string" ? pluginOrName : pluginOrName.name;
11577
+ const plugin = this.__plugins[name2];
11578
+ delete this.__plugins[name2];
11579
+ this.events.dispatch({ type: `plugin:remove:${this.__type}` });
11580
+ return plugin;
11581
+ }
11582
+ subscribe(cb) {
11583
+ return this.events.subscribe(`plugin:*:${this.__type}`, cb);
11256
11584
  }
11257
11585
  }
11258
11586
  const _CMS = class _CMS {
@@ -11365,14 +11693,28 @@ const _CMS = class _CMS {
11365
11693
  this.unsubscribeHooks[name2]();
11366
11694
  }
11367
11695
  if (api.events instanceof EventBus) {
11368
- const unsubscribeHost = api.events.subscribe(
11369
- "*",
11370
- this.events.dispatch
11371
- );
11372
- const unsubscribeGuest = this.events.subscribe(
11373
- "*",
11374
- (e) => api.events.dispatch(e)
11375
- );
11696
+ let forwardingToCms = null;
11697
+ let forwardingToApi = null;
11698
+ const unsubscribeHost = api.events.subscribe("*", (e) => {
11699
+ if (e === forwardingToApi) return;
11700
+ const previous = forwardingToCms;
11701
+ forwardingToCms = e;
11702
+ try {
11703
+ this.events.dispatch(e);
11704
+ } finally {
11705
+ forwardingToCms = previous;
11706
+ }
11707
+ });
11708
+ const unsubscribeGuest = this.events.subscribe("*", (e) => {
11709
+ if (e === forwardingToCms) return;
11710
+ const previous = forwardingToApi;
11711
+ forwardingToApi = e;
11712
+ try {
11713
+ api.events.dispatch(e);
11714
+ } finally {
11715
+ forwardingToApi = previous;
11716
+ }
11717
+ });
11376
11718
  this.unsubscribeHooks[name2] = () => {
11377
11719
  unsubscribeHost();
11378
11720
  unsubscribeGuest();
@@ -11715,6 +12057,10 @@ const RenameModal$1 = ({
11715
12057
  await renameFunc(sanitized);
11716
12058
  close2();
11717
12059
  } catch (e) {
12060
+ if ((e == null ? void 0 : e.ERR_TYPE) === "MediaRenameCancelled") {
12061
+ close2();
12062
+ return;
12063
+ }
11718
12064
  setError(renameErrorMessage(e, sanitized));
11719
12065
  setProcessing(false);
11720
12066
  }
@@ -11824,6 +12170,7 @@ function MediaPicker({
11824
12170
  allowDelete,
11825
12171
  onSelect,
11826
12172
  close: close2,
12173
+ accept: fieldAccept,
11827
12174
  ...props
11828
12175
  }) {
11829
12176
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
@@ -11860,6 +12207,7 @@ function MediaPicker({
11860
12207
  const [mediaFilter, setMediaFilter] = useState(
11861
12208
  "all"
11862
12209
  );
12210
+ const [typeFilter, setTypeFilter] = useState("all");
11863
12211
  const [offsetHistory, setOffsetHistory] = useState([]);
11864
12212
  const offset2 = offsetHistory[offsetHistory.length - 1];
11865
12213
  const resetOffset = () => setOffsetHistory([]);
@@ -11872,8 +12220,25 @@ function MediaPicker({
11872
12220
  resetList();
11873
12221
  setActiveItem(false);
11874
12222
  };
12223
+ const changeTypeFilter = (next) => {
12224
+ if (next === typeFilter) return;
12225
+ setTypeFilter(next);
12226
+ setLoadFolders(true);
12227
+ resetOffset();
12228
+ resetList();
12229
+ setActiveItem(false);
12230
+ };
11875
12231
  const listRequestRef = useRef(0);
11876
12232
  const newMediaSrcsRef = useRef(/* @__PURE__ */ new Set());
12233
+ const fieldExtensions = React__default.useMemo(
12234
+ () => resolveMediaAccept(fieldAccept),
12235
+ [fieldAccept]
12236
+ );
12237
+ const acceptedExtensions = React__default.useMemo(() => {
12238
+ if (!cms.media.store.extensionFilterable) return [];
12239
+ if (fieldExtensions.length) return fieldExtensions;
12240
+ return typeFilter === "all" ? [] : resolveMediaAccept(typeFilter);
12241
+ }, [fieldExtensions, typeFilter, cms.media.store.extensionFilterable]);
11877
12242
  async function loadMedia(loadFolders2 = true) {
11878
12243
  const requestId = ++listRequestRef.current;
11879
12244
  setListState("loading");
@@ -11888,7 +12253,8 @@ function MediaPicker({
11888
12253
  { w: 1e3, h: 1e3 }
11889
12254
  ],
11890
12255
  filesOnly: !loadFolders2,
11891
- search: debouncedSearch || void 0
12256
+ search: debouncedSearch || void 0,
12257
+ ext: acceptedExtensions.length ? acceptedExtensions : void 0
11892
12258
  });
11893
12259
  if (requestId !== listRequestRef.current) return;
11894
12260
  setList((prev) => {
@@ -11931,9 +12297,6 @@ function MediaPicker({
11931
12297
  }, [refreshing]);
11932
12298
  useEffect(() => {
11933
12299
  if (!cms.media.isConfigured) return;
11934
- if (refreshing) return;
11935
- loadMedia(loadFolders);
11936
- if (loadFolders) setLoadFolders(false);
11937
12300
  return cms.events.subscribe(
11938
12301
  ["media:delete:success", "media:rename:success", "media:pageSize"],
11939
12302
  (event) => {
@@ -11947,7 +12310,19 @@ function MediaPicker({
11947
12310
  resetList();
11948
12311
  }
11949
12312
  );
11950
- }, [offset2, directory, debouncedSearch, cms.media.isConfigured]);
12313
+ }, [cms.media.isConfigured]);
12314
+ useEffect(() => {
12315
+ if (!cms.media.isConfigured) return;
12316
+ if (refreshing) return;
12317
+ loadMedia(loadFolders);
12318
+ if (loadFolders) setLoadFolders(false);
12319
+ }, [
12320
+ offset2,
12321
+ directory,
12322
+ debouncedSearch,
12323
+ acceptedExtensions,
12324
+ cms.media.isConfigured
12325
+ ]);
11951
12326
  const onClickMediaItem = (item) => {
11952
12327
  if (!item) {
11953
12328
  setActiveItem(false);
@@ -11981,12 +12356,12 @@ function MediaPicker({
11981
12356
  };
11982
12357
  }
11983
12358
  const [uploading, setUploading] = useState(false);
11984
- const accept = Array.isArray(
12359
+ const globalAccept = Array.isArray(
11985
12360
  (_c = (_b = (_a = cms.api.tina.schema.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.media) == null ? void 0 : _c.accept
11986
12361
  ) ? (_f = (_e = (_d = cms.api.tina.schema.schema) == null ? void 0 : _d.config) == null ? void 0 : _e.media) == null ? void 0 : _f.accept.join(",") : (_i = (_h = (_g = cms.api.tina.schema.schema) == null ? void 0 : _g.config) == null ? void 0 : _h.media) == null ? void 0 : _i.accept;
11987
12362
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
11988
- accept: dropzoneAcceptFromString(
11989
- accept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
12363
+ accept: dropzoneAcceptFromExtensions(fieldExtensions) ?? dropzoneAcceptFromString(
12364
+ globalAccept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
11990
12365
  ),
11991
12366
  maxSize: cms.media.maxSize,
11992
12367
  multiple: true,
@@ -12203,7 +12578,15 @@ function MediaPicker({
12203
12578
  setValue: setSearch,
12204
12579
  clear: () => setSearch("")
12205
12580
  }
12206
- ), /* @__PURE__ */ React__default.createElement("div", { className: "ml-auto" }, /* @__PURE__ */ React__default.createElement(
12581
+ ), /* @__PURE__ */ React__default.createElement("div", { className: "ml-auto flex items-center gap-3" }, cms.media.store.extensionFilterable && /* @__PURE__ */ React__default.createElement(
12582
+ MediaTypeFilter,
12583
+ {
12584
+ value: typeFilter,
12585
+ setValue: changeTypeFilter,
12586
+ lockedTo: fieldAccept,
12587
+ lockedExtensions: fieldExtensions
12588
+ }
12589
+ ), /* @__PURE__ */ React__default.createElement(
12207
12590
  MediaFilterToggle,
12208
12591
  {
12209
12592
  value: mediaFilter,
@@ -12468,6 +12851,62 @@ const MediaFilterToggle = ({
12468
12851
  option.label
12469
12852
  )));
12470
12853
  };
12854
+ const TYPE_FILTER_LABELS = {
12855
+ image: "Images",
12856
+ video: "Video",
12857
+ audio: "Audio",
12858
+ document: "Documents"
12859
+ };
12860
+ const acceptEntryLabel = (entry) => entry in MEDIA_CATEGORIES ? TYPE_FILTER_LABELS[entry] : entry.toUpperCase();
12861
+ const MediaTypeFilter = ({
12862
+ value,
12863
+ setValue,
12864
+ lockedTo,
12865
+ lockedExtensions
12866
+ }) => {
12867
+ if (lockedExtensions.length) {
12868
+ const entries = Array.isArray(lockedTo) ? lockedTo : [lockedTo];
12869
+ const label = entries.length > 3 ? `${entries.length} types only` : `${entries.map(acceptEntryLabel).join(", ")} only`;
12870
+ const description = `This field accepts ${lockedExtensions.join(", ")}`;
12871
+ return /* @__PURE__ */ React__default.createElement(
12872
+ "span",
12873
+ {
12874
+ className: "grow-0 flex items-center gap-1.5 whitespace-nowrap rounded-lg border border-dashed border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-500",
12875
+ title: description
12876
+ },
12877
+ /* @__PURE__ */ React__default.createElement(Lock, { className: "w-3.5 h-3.5 opacity-80", "aria-hidden": "true" }),
12878
+ label,
12879
+ /* @__PURE__ */ React__default.createElement("span", { className: "sr-only" }, description)
12880
+ );
12881
+ }
12882
+ const active = value !== "all";
12883
+ return /* @__PURE__ */ React__default.createElement(DropdownMenu, null, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger, { asChild: true }, /* @__PURE__ */ React__default.createElement(
12884
+ "button",
12885
+ {
12886
+ type: "button",
12887
+ className: `grow-0 flex items-center gap-2 whitespace-nowrap rounded-lg border bg-white px-3 py-1.5 text-sm transition-colors ease-out duration-150 ${active ? "border-tina-orange text-tina-orange font-medium" : "border-gray-200 text-gray-700 hover:border-gray-300"}`
12888
+ },
12889
+ active ? TYPE_FILTER_LABELS[value] : "Any type",
12890
+ /* @__PURE__ */ React__default.createElement(
12891
+ ChevronDown,
12892
+ {
12893
+ className: `w-4 h-4 ${active ? "opacity-90" : "text-gray-400"}`
12894
+ }
12895
+ )
12896
+ )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent, { align: "end", className: "min-w-52" }, /* @__PURE__ */ React__default.createElement(
12897
+ DropdownMenuRadioGroup,
12898
+ {
12899
+ value,
12900
+ onValueChange: (next) => setValue(next)
12901
+ },
12902
+ /* @__PURE__ */ React__default.createElement(DropdownMenuRadioItem, { value: "all" }, "Any type"),
12903
+ /* @__PURE__ */ React__default.createElement(DropdownMenuSeparator, null),
12904
+ /* @__PURE__ */ React__default.createElement(DropdownMenuLabel, null, "Category"),
12905
+ Object.keys(MEDIA_CATEGORIES).map(
12906
+ (category) => /* @__PURE__ */ React__default.createElement(DropdownMenuRadioItem, { key: category, value: category }, TYPE_FILTER_LABELS[category])
12907
+ )
12908
+ )));
12909
+ };
12471
12910
  const ViewModeToggle = ({ viewMode, setViewMode }) => {
12472
12911
  const base = "relative flex items-center justify-center w-9 py-1.5 transition-colors ease-out duration-150";
12473
12912
  const active = "bg-white text-tina-orange";
@@ -13719,7 +14158,7 @@ const FormModal = ({ plugin, close: close2 }) => {
13719
14158
  const name = "tinacms";
13720
14159
  const type = "module";
13721
14160
  const typings = "dist/index.d.ts";
13722
- const version = "3.12.1";
14161
+ const version = "3.13.0";
13723
14162
  const main = "dist/index.js";
13724
14163
  const module$1 = "./dist/index.js";
13725
14164
  const exports$1 = { ".": "./dist/index.js", "./dist/client": "./dist/client.js", "./react": "./dist/react.js", "./dist/react": "./dist/react.js", "./tina-field": "./dist/tina-field.js", "./dist/tina-field": "./dist/tina-field.js", "./dist/rich-text": "./dist/rich-text/index.js", "./dist/rich-text/static": "./dist/rich-text/static.js", "./dist/rich-text/prism": "./dist/rich-text/prism.js" };
@@ -13827,7 +14266,11 @@ const useGetEvents = (cms, cursor, existingEvents) => {
13827
14266
  var _a, _b, _c, _d, _e;
13828
14267
  let doFetchEvents = false;
13829
14268
  if (!((_b = (_a = cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isCustomContentApi)) {
13830
- doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14269
+ try {
14270
+ doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14271
+ } catch {
14272
+ doFetchEvents = false;
14273
+ }
13831
14274
  }
13832
14275
  if (doFetchEvents) {
13833
14276
  try {
@@ -13860,7 +14303,11 @@ function useSyncStatus(cms) {
13860
14303
  var _a2, _b2, _c, _d, _e;
13861
14304
  let doFetchEvents = false;
13862
14305
  if (!((_b2 = (_a2 = cms.api) == null ? void 0 : _a2.tina) == null ? void 0 : _b2.isCustomContentApi)) {
13863
- doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14306
+ try {
14307
+ doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14308
+ } catch {
14309
+ doFetchEvents = false;
14310
+ }
13864
14311
  }
13865
14312
  if (doFetchEvents) {
13866
14313
  const { events } = await cms.api.tina.fetchEvents();
@@ -17144,6 +17591,35 @@ const CreateBranchModal = ({
17144
17591
  }
17145
17592
  );
17146
17593
  };
17594
+ const getInitials = (name2) => {
17595
+ const atIndex = name2.indexOf("@");
17596
+ const localPart = atIndex === -1 ? name2 : name2.slice(0, atIndex);
17597
+ return localPart.split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((part) => [...part][0].toUpperCase()).join("");
17598
+ };
17599
+ const CommittingAs = () => {
17600
+ var _a, _b, _c;
17601
+ const cms = useCMS$1();
17602
+ const user = (_b = (_a = cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.user;
17603
+ const author = typeof user === "object" && user !== null ? user : void 0;
17604
+ const mode = (_c = author == null ? void 0 : author.gitAuthoring) == null ? void 0 : _c.mode;
17605
+ if (mode !== "bot" && mode !== "user") {
17606
+ return null;
17607
+ }
17608
+ const authorName = mode === "bot" ? "TinaCloud bot" : author.fullName || author.email;
17609
+ if (!authorName) {
17610
+ return null;
17611
+ }
17612
+ return /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 mt-4 pt-4 border-t border-gray-100" }, /* @__PURE__ */ React.createElement("span", { className: "flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-600 text-xs font-medium flex-shrink-0" }, getInitials(authorName)), /* @__PURE__ */ React.createElement("div", { className: "flex-1 min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-gray-500" }, "Committing as"), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-gray-700 font-medium truncate" }, authorName)), /* @__PURE__ */ React.createElement(
17613
+ "a",
17614
+ {
17615
+ className: "text-sm underline text-tina-orange-dark font-medium flex-shrink-0",
17616
+ href: cms.api.tina.gitSettingsLink,
17617
+ target: "_blank",
17618
+ rel: "noreferrer"
17619
+ },
17620
+ "Change"
17621
+ ));
17622
+ };
17147
17623
  const CreateBranchPromptModal = ({
17148
17624
  branchName,
17149
17625
  close: close2,
@@ -17153,7 +17629,8 @@ const CreateBranchPromptModal = ({
17153
17629
  onCreateBranch,
17154
17630
  onSaveToProtectedBranch,
17155
17631
  showSaveOptions = false,
17156
- disablePublish = false
17632
+ disablePublish = false,
17633
+ allowSaveToProtectedBranch = true
17157
17634
  }) => {
17158
17635
  const [lastChoice, setLastChoice] = useLocalStorage(
17159
17636
  SAVE_CHOICE_KEY,
@@ -17200,7 +17677,7 @@ const CreateBranchPromptModal = ({
17200
17677
  onBranchNameChange(e.target.value);
17201
17678
  }
17202
17679
  }
17203
- ))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17680
+ ), /* @__PURE__ */ React.createElement(CommittingAs, null))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17204
17681
  Button$2,
17205
17682
  {
17206
17683
  variant: "secondary",
@@ -17233,7 +17710,7 @@ const CreateBranchPromptModal = ({
17233
17710
  },
17234
17711
  /* @__PURE__ */ React.createElement(MainIcon, { className: "w-4 h-4 mr-1", style: { fill: "none" } }),
17235
17712
  mainChoice.label
17236
- ) : /* @__PURE__ */ React.createElement(
17713
+ ) : allowSaveToProtectedBranch ? /* @__PURE__ */ React.createElement(
17237
17714
  DropdownButton,
17238
17715
  {
17239
17716
  variant: "primary",
@@ -17257,6 +17734,25 @@ const CreateBranchPromptModal = ({
17257
17734
  }
17258
17735
  ),
17259
17736
  "Save to a new branch"
17737
+ ) : (
17738
+ // A plain button: the dropdown's only item is the one being dropped.
17739
+ /* @__PURE__ */ React.createElement(
17740
+ Button$2,
17741
+ {
17742
+ variant: "primary",
17743
+ className: "w-full sm:w-auto",
17744
+ disabled,
17745
+ onClick: () => onCreateBranch(false)
17746
+ },
17747
+ /* @__PURE__ */ React.createElement(
17748
+ GitBranchIcon,
17749
+ {
17750
+ className: "w-4 h-4 mr-1",
17751
+ style: { fill: "none" }
17752
+ }
17753
+ ),
17754
+ "Save to a new branch"
17755
+ )
17260
17756
  ))));
17261
17757
  };
17262
17758
  const PrefixedTextField = ({
@@ -17609,6 +18105,10 @@ const FormBuilder = ({
17609
18105
  setDeletedBranchModalOpen(true);
17610
18106
  return;
17611
18107
  }
18108
+ if (isSessionExpiredError(error)) {
18109
+ captureWorkflowChoice(false, errorMsg);
18110
+ return;
18111
+ }
17612
18112
  captureEvent(SaveContentErrorEvent, {
17613
18113
  documentPath: tinaForm.path,
17614
18114
  error: errorMsg
@@ -18709,7 +19209,7 @@ function HeadingsMenu(props) {
18709
19209
  className: "min-w-0 rounded-none rounded-bl rounded-br border-border"
18710
19210
  },
18711
19211
  /* @__PURE__ */ React__default.createElement(
18712
- DropdownMenuRadioGroup,
19212
+ DropdownMenuRadioGroup$1,
18713
19213
  {
18714
19214
  className: "flex flex-col gap-0.5",
18715
19215
  onValueChange: (type2) => {
@@ -18725,7 +19225,7 @@ function HeadingsMenu(props) {
18725
19225
  }
18726
19226
  return true;
18727
19227
  }).map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
18728
- DropdownMenuRadioItem,
19228
+ DropdownMenuRadioItem$1,
18729
19229
  {
18730
19230
  className: "min-w-[180px]",
18731
19231
  key: itemValue,
@@ -19481,549 +19981,6 @@ const useCreateEditor = ({
19481
19981
  components
19482
19982
  });
19483
19983
  };
19484
- const popoverVariants = cva(
19485
- "z-[999999] w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
19486
- );
19487
- const inputVariants = cva(
19488
- "flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:ring-transparent focus-visible:outline-none md:text-sm"
19489
- );
19490
- function LinkFloatingToolbar({
19491
- state
19492
- }) {
19493
- const activeCommentId = usePluginOption({ key: "comment" }, "activeId");
19494
- const activeSuggestionId = usePluginOption({ key: "suggestion" }, "activeId");
19495
- const { api, editor } = useEditorPlugin(LinkPlugin);
19496
- const isUrlValidator = usePluginOption(LinkPlugin, "isUrl");
19497
- const [currentUrl, setCurrentUrl] = React.useState("");
19498
- const [isValidUrl, setIsValidUrl] = React.useState(true);
19499
- const handleUrlInput = React.useCallback(
19500
- (e) => {
19501
- const value = e.currentTarget.value;
19502
- setCurrentUrl(value);
19503
- if (value && isUrlValidator) {
19504
- const valid = isUrlValidator(value);
19505
- setIsValidUrl(valid);
19506
- } else {
19507
- setIsValidUrl(true);
19508
- }
19509
- },
19510
- [isUrlValidator]
19511
- );
19512
- const floatingOptions = React.useMemo(() => {
19513
- return {
19514
- middleware: [
19515
- offset(8),
19516
- flip({
19517
- fallbackPlacements: ["bottom-end", "top-start", "top-end"],
19518
- padding: 12
19519
- }),
19520
- shift({ padding: 8, limiter: limitShift() })
19521
- ],
19522
- placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start"
19523
- };
19524
- }, [activeCommentId, activeSuggestionId]);
19525
- const insertState = useFloatingLinkInsertState({
19526
- ...state,
19527
- floatingOptions: {
19528
- ...floatingOptions,
19529
- ...state == null ? void 0 : state.floatingOptions
19530
- }
19531
- });
19532
- const {
19533
- hidden,
19534
- props: insertProps,
19535
- ref: insertRef,
19536
- textInputProps
19537
- } = useFloatingLinkInsert(insertState);
19538
- const editState = useFloatingLinkEditState({
19539
- ...state,
19540
- floatingOptions: {
19541
- ...floatingOptions,
19542
- ...state == null ? void 0 : state.floatingOptions
19543
- }
19544
- });
19545
- const {
19546
- editButtonProps,
19547
- props: editProps,
19548
- ref: editRef,
19549
- unlinkButtonProps
19550
- } = useFloatingLinkEdit(editState);
19551
- const inputProps = useFormInputProps({
19552
- preventDefaultOnEnterKeydown: true
19553
- });
19554
- if (hidden) return null;
19555
- const input = /* @__PURE__ */ React.createElement(
19556
- "div",
19557
- {
19558
- className: "z-[999999] flex w-[330px] flex-col relative",
19559
- ...inputProps
19560
- },
19561
- !isValidUrl && currentUrl && /* @__PURE__ */ React.createElement("div", { className: "absolute -top-16 left-0 right-0 z-[1000000] mb-2" }, /* @__PURE__ */ React.createElement(
19562
- "div",
19563
- {
19564
- className: "bg-red-50 border border-red-200 rounded-md p-2 shadow-lg",
19565
- role: "alert",
19566
- "aria-live": "polite"
19567
- },
19568
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement(CircleX, { className: "size-4 text-red-500 mr-2 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-red-700 text-wrap" }, "Invalid URL. Please prefix link with https:// or use a relative path like /about")),
19569
- /* @__PURE__ */ React.createElement("div", { className: "absolute -bottom-1 left-4 w-2 h-2 bg-red-50 border-r border-b border-red-200 transform rotate-45" })
19570
- )),
19571
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Link, { className: "size-4" })), /* @__PURE__ */ React.createElement(
19572
- FloatingLinkUrlInput,
19573
- {
19574
- className: inputVariants(),
19575
- placeholder: "Paste link",
19576
- "data-plate-focus": true,
19577
- onInput: handleUrlInput
19578
- }
19579
- )),
19580
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19581
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Text, { className: "size-4" })), /* @__PURE__ */ React.createElement(
19582
- "input",
19583
- {
19584
- className: inputVariants(),
19585
- placeholder: "Text to display",
19586
- "data-plate-focus": true,
19587
- ...textInputProps
19588
- }
19589
- )),
19590
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19591
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-2 px-2 py-1" }, /* @__PURE__ */ React.createElement(
19592
- "button",
19593
- {
19594
- type: "button",
19595
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19596
- onClick: () => {
19597
- api.floatingLink.hide();
19598
- }
19599
- },
19600
- "Cancel"
19601
- ), /* @__PURE__ */ React.createElement(
19602
- "button",
19603
- {
19604
- type: "button",
19605
- className: buttonVariants$2({
19606
- size: "sm",
19607
- variant: "tinaPrimary"
19608
- }),
19609
- onClick: () => {
19610
- if (isValidUrl && currentUrl) {
19611
- submitFloatingLink(editor);
19612
- }
19613
- },
19614
- disabled: !isValidUrl && !!currentUrl
19615
- },
19616
- "OK"
19617
- ))
19618
- );
19619
- const editContent = editState.isEditing ? input : /* @__PURE__ */ React.createElement("div", { className: "box-content flex items-center" }, /* @__PURE__ */ React.createElement(
19620
- "button",
19621
- {
19622
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19623
- type: "button",
19624
- ...editButtonProps
19625
- },
19626
- "Edit link"
19627
- ), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(LinkOpenButton, null), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(
19628
- "button",
19629
- {
19630
- className: buttonVariants$2({
19631
- size: "icon",
19632
- variant: "ghost"
19633
- }),
19634
- type: "button",
19635
- ...unlinkButtonProps
19636
- },
19637
- /* @__PURE__ */ React.createElement(Unlink, { width: 18 })
19638
- ));
19639
- return /* @__PURE__ */ React.createElement(PortalBody, null, /* @__PURE__ */ React.createElement(
19640
- "div",
19641
- {
19642
- ref: insertRef,
19643
- className: popoverVariants(),
19644
- ...insertProps,
19645
- style: {
19646
- ...insertProps.style,
19647
- zIndex: 999999
19648
- }
19649
- },
19650
- input
19651
- ), /* @__PURE__ */ React.createElement(
19652
- "div",
19653
- {
19654
- ref: editRef,
19655
- className: popoverVariants(),
19656
- ...editProps,
19657
- style: { ...editProps.style, zIndex: 999999 }
19658
- },
19659
- editContent
19660
- ));
19661
- }
19662
- function LinkOpenButton() {
19663
- const editor = useEditorRef();
19664
- const selection = useEditorSelection();
19665
- const attributes = React.useMemo(
19666
- () => {
19667
- const entry = editor.api.node({
19668
- match: { type: editor.getType(LinkPlugin) }
19669
- });
19670
- if (!entry) {
19671
- return {};
19672
- }
19673
- const [element] = entry;
19674
- return getLinkAttributes(editor, element);
19675
- },
19676
- // eslint-disable-next-line react-hooks/exhaustive-deps
19677
- [editor, selection]
19678
- );
19679
- return /* @__PURE__ */ React.createElement(
19680
- "a",
19681
- {
19682
- ...attributes,
19683
- className: buttonVariants$2({
19684
- size: "icon",
19685
- variant: "ghost"
19686
- }),
19687
- onMouseOver: (e) => {
19688
- e.stopPropagation();
19689
- },
19690
- "aria-label": "Open link in a new tab",
19691
- target: "_blank"
19692
- },
19693
- /* @__PURE__ */ React.createElement(ExternalLink, { width: 18 })
19694
- );
19695
- }
19696
- const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
19697
- const emailLintRE = /mailto:([^?\\]+)/;
19698
- const telLintRE = /tel:([\d-]+)/;
19699
- const localhostDomainRE = /^localhost[\d:?]*(?:[^\d:?]\S*)?$/;
19700
- const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/;
19701
- const localUrlRE = /^\/\S+/;
19702
- const isUrl = (string) => {
19703
- if (typeof string !== "string") {
19704
- return false;
19705
- }
19706
- if (string.startsWith("#")) {
19707
- return true;
19708
- }
19709
- const generalMatch = string.match(protocolAndDomainRE);
19710
- const emailLinkMatch = string.match(emailLintRE);
19711
- const telLinkMatch = string.match(telLintRE);
19712
- const localUrlMatch = string.match(localUrlRE);
19713
- if (emailLinkMatch || telLinkMatch || localUrlMatch) {
19714
- return true;
19715
- }
19716
- if (generalMatch) {
19717
- const everythingAfterProtocol = generalMatch[1];
19718
- if (!everythingAfterProtocol) {
19719
- return false;
19720
- }
19721
- try {
19722
- new URL(string);
19723
- } catch {
19724
- return false;
19725
- }
19726
- return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol);
19727
- }
19728
- return false;
19729
- };
19730
- const buildError = (element) => {
19731
- return {
19732
- message: element.message,
19733
- position: element.position && {
19734
- endColumn: element.position.end.column,
19735
- startColumn: element.position.start.column,
19736
- startLineNumber: element.position.start.line,
19737
- endLineNumber: element.position.end.line
19738
- }
19739
- };
19740
- };
19741
- const buildErrorMessage = (element) => {
19742
- if (!element) {
19743
- return "";
19744
- }
19745
- const errorMessage = buildError(element);
19746
- const message = errorMessage ? `${errorMessage.message}${errorMessage.position ? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}` : ""}` : null;
19747
- return message;
19748
- };
19749
- const ELEMENT_INVALID_MARKDOWN = "invalid_markdown";
19750
- const createInvalidMarkdownPlugin = createPlatePlugin({
19751
- key: ELEMENT_INVALID_MARKDOWN,
19752
- options: {
19753
- isElement: true,
19754
- isVoid: true,
19755
- isInline: false
19756
- },
19757
- node: {
19758
- component: InvalidMarkdownElement
19759
- }
19760
- });
19761
- function InvalidMarkdownElement({
19762
- attributes,
19763
- element,
19764
- children
19765
- }) {
19766
- return /* @__PURE__ */ React__default.createElement("div", { ...attributes }, /* @__PURE__ */ React__default.createElement(ErrorMessage, { error: element }), children);
19767
- }
19768
- function ErrorMessage({ error }) {
19769
- const message = buildErrorMessage(error);
19770
- const { setRawMode } = useEditorContext();
19771
- return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "bg-red-50 sm:rounded-lg" }, /* @__PURE__ */ React__default.createElement("div", { className: "px-4 py-5 sm:p-6" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg leading-6 font-medium text-red-800" }, "❌ Error parsing markdown"), /* @__PURE__ */ React__default.createElement("div", { className: "mt-2 max-w-xl text-sm text-red-800 space-y-4" }, /* @__PURE__ */ React__default.createElement("p", null, message), /* @__PURE__ */ React__default.createElement("p", null, "To fix these errors, edit the content in raw-mode."), /* @__PURE__ */ React__default.createElement(
19772
- "button",
19773
- {
19774
- type: "button",
19775
- onClick: () => setRawMode(true),
19776
- className: "rounded-l border-r-0 shadow rounded bg-white cursor-pointer relative inline-flex items-center px-2 py-2 border border-gray-200 hover:text-white text-sm font-medium transition-all ease-out duration-150 hover:bg-gray-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"
19777
- },
19778
- "Switch to raw-mode"
19779
- ))));
19780
- }
19781
- const useCustomFloatingToolbarState = ({
19782
- editorId,
19783
- floatingOptions,
19784
- focusedEditorId,
19785
- hideToolbar,
19786
- showWhenReadOnly
19787
- }) => {
19788
- const editor = useEditorRef();
19789
- const selectionExpanded = useEditorSelector(
19790
- () => editor.api.isExpanded(),
19791
- []
19792
- );
19793
- const selectionText = useEditorSelector(() => editor.api.string(), []);
19794
- const readOnly = useEditorReadOnly();
19795
- const focused = useFocused();
19796
- const [open, setOpen] = React__default.useState(false);
19797
- const [waitForCollapsedSelection, setWaitForCollapsedSelection] = React__default.useState(false);
19798
- const [mousedown, setMousedown] = React__default.useState(false);
19799
- const floating = useVirtualFloating(
19800
- mergeProps(
19801
- {
19802
- open,
19803
- getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
19804
- onOpenChange: setOpen
19805
- },
19806
- floatingOptions
19807
- )
19808
- );
19809
- return {
19810
- editorId,
19811
- floating,
19812
- focused,
19813
- focusedEditorId,
19814
- hideToolbar,
19815
- mousedown,
19816
- open,
19817
- readOnly,
19818
- selectionExpanded,
19819
- selectionText,
19820
- setMousedown,
19821
- setOpen,
19822
- setWaitForCollapsedSelection,
19823
- showWhenReadOnly,
19824
- waitForCollapsedSelection
19825
- };
19826
- };
19827
- const useCustomFloatingToolbar = ({
19828
- editorId,
19829
- floating,
19830
- focusedEditorId,
19831
- hideToolbar,
19832
- mousedown,
19833
- open,
19834
- readOnly,
19835
- selectionExpanded,
19836
- selectionText,
19837
- setMousedown,
19838
- setOpen,
19839
- setWaitForCollapsedSelection,
19840
- showWhenReadOnly,
19841
- waitForCollapsedSelection
19842
- }) => {
19843
- React__default.useEffect(() => {
19844
- if (!(editorId === focusedEditorId)) {
19845
- setWaitForCollapsedSelection(true);
19846
- }
19847
- if (!selectionExpanded) {
19848
- setWaitForCollapsedSelection(false);
19849
- }
19850
- }, [
19851
- editorId,
19852
- focusedEditorId,
19853
- selectionExpanded,
19854
- setWaitForCollapsedSelection
19855
- ]);
19856
- React__default.useEffect(() => {
19857
- const mouseup = () => setMousedown(false);
19858
- const mousedown2 = () => setMousedown(true);
19859
- document.addEventListener("mouseup", mouseup);
19860
- document.addEventListener("mousedown", mousedown2);
19861
- return () => {
19862
- document.removeEventListener("mouseup", mouseup);
19863
- document.removeEventListener("mousedown", mousedown2);
19864
- };
19865
- }, []);
19866
- React__default.useEffect(() => {
19867
- if (!selectionExpanded || !selectionText || mousedown && !open || hideToolbar || readOnly && !showWhenReadOnly) {
19868
- setOpen(false);
19869
- } else if (selectionText && selectionExpanded && (!waitForCollapsedSelection || readOnly)) {
19870
- setOpen(true);
19871
- }
19872
- }, [
19873
- setOpen,
19874
- editorId,
19875
- focusedEditorId,
19876
- hideToolbar,
19877
- showWhenReadOnly,
19878
- selectionExpanded,
19879
- selectionText,
19880
- mousedown,
19881
- waitForCollapsedSelection,
19882
- open,
19883
- readOnly
19884
- ]);
19885
- const { update } = floating;
19886
- useEditorSelector(() => {
19887
- update == null ? void 0 : update();
19888
- }, [update]);
19889
- const clickOutsideRef = useOnClickOutside(
19890
- () => {
19891
- setOpen(false);
19892
- },
19893
- {
19894
- ignoreClass: "ignore-click-outside/toolbar"
19895
- }
19896
- );
19897
- return {
19898
- clickOutsideRef,
19899
- hidden: !open,
19900
- props: {
19901
- style: floating.style
19902
- },
19903
- ref: floating.refs.setFloating
19904
- };
19905
- };
19906
- const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
19907
- const editorId = useEditorRef();
19908
- const focusedEditorId = useEventEditorValue("focus");
19909
- const test = useCustomFloatingToolbarState({
19910
- editorId: editorId.id,
19911
- focusedEditorId,
19912
- ...state,
19913
- floatingOptions: {
19914
- middleware: [
19915
- offset(12),
19916
- flip({
19917
- fallbackPlacements: [
19918
- "top-start",
19919
- "top-end",
19920
- "bottom-start",
19921
- "bottom-end"
19922
- ],
19923
- padding: 12
19924
- })
19925
- ],
19926
- placement: "top",
19927
- ...state == null ? void 0 : state.floatingOptions
19928
- }
19929
- });
19930
- const {
19931
- hidden,
19932
- props: rootProps,
19933
- ref: floatingRef
19934
- } = useCustomFloatingToolbar(test);
19935
- const ref = useComposedRef$1(propRef, floatingRef);
19936
- if (hidden) return null;
19937
- return /* @__PURE__ */ React__default.createElement(PortalBody, null, /* @__PURE__ */ React__default.createElement(
19938
- Toolbar,
19939
- {
19940
- className: cn$1(
19941
- "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
19942
- ),
19943
- ...props,
19944
- ...rootProps,
19945
- ref
19946
- },
19947
- children
19948
- ));
19949
- });
19950
- function TurnIntoDropdownMenu(props) {
19951
- const { headingLevels } = useToolbarContext();
19952
- const items = [
19953
- paragraphItem,
19954
- ...headingLevels.map((level) => headingItemsByLevel[level])
19955
- ];
19956
- const value = useEditorSelector((editor2) => {
19957
- let initialNodeType = ParagraphPlugin.key;
19958
- let allNodesMatchInitialNodeType = false;
19959
- const codeBlockEntries = editor2.api.nodes({
19960
- match: (n) => editor2.api.isBlock(n),
19961
- mode: "highest"
19962
- });
19963
- const nodes = Array.from(codeBlockEntries);
19964
- if (nodes.length > 0) {
19965
- initialNodeType = nodes[0][0].type;
19966
- allNodesMatchInitialNodeType = nodes.every(([node]) => {
19967
- const type2 = (node == null ? void 0 : node.type) || ParagraphPlugin.key;
19968
- return type2 === initialNodeType;
19969
- });
19970
- }
19971
- return allNodesMatchInitialNodeType ? initialNodeType : ParagraphPlugin.key;
19972
- }, []);
19973
- const editor = useEditorRef();
19974
- const openState = useOpenState();
19975
- const selectedItem = getHeadingItem(value) ?? paragraphItem;
19976
- const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
19977
- const editorState = useEditorState();
19978
- const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
19979
- if (userInTable) return null;
19980
- return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
19981
- ToolbarButton,
19982
- {
19983
- className: "lg:min-w-[130px]",
19984
- isDropdown: true,
19985
- showArrow: true,
19986
- pressed: openState.open,
19987
- tooltip: "Turn into"
19988
- },
19989
- /* @__PURE__ */ React__default.createElement("span", { className: "" }, selectedItemLabel)
19990
- )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-0" }, /* @__PURE__ */ React__default.createElement(DropdownMenuLabel, null, "Turn into"), /* @__PURE__ */ React__default.createElement(
19991
- DropdownMenuRadioGroup,
19992
- {
19993
- className: "flex flex-col gap-0.5",
19994
- onValueChange: (type2) => {
19995
- if (type2 === "ul" || type2 === "ol") {
19996
- toggleList(editor, { type: type2 });
19997
- } else {
19998
- unwrapList(editor);
19999
- editor.tf.toggleBlock(type2);
20000
- }
20001
- editor.tf.collapse();
20002
- editor.tf.focus();
20003
- },
20004
- value
20005
- },
20006
- items.map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
20007
- DropdownMenuRadioItem,
20008
- {
20009
- className: "min-w-[180px]",
20010
- key: itemValue,
20011
- value: itemValue
20012
- },
20013
- /* @__PURE__ */ React__default.createElement(Icon, { className: "mr-2 size-5" }),
20014
- label
20015
- ))
20016
- )));
20017
- }
20018
- function FloatingToolbarButtons() {
20019
- return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(TurnIntoDropdownMenu, null));
20020
- }
20021
- const FloatingToolbarPlugin = createPlatePlugin({
20022
- key: "floating-toolbar",
20023
- render: {
20024
- afterEditable: () => /* @__PURE__ */ React__default.createElement(FloatingToolbar, null, /* @__PURE__ */ React__default.createElement(FloatingToolbarButtons, null))
20025
- }
20026
- });
20027
19984
  function _1c(hljs) {
20028
19985
  const UNDERSCORE_IDENT_RE = "[A-Za-zА-Яа-яёЁ_][A-Za-zА-Яа-яёЁ_0-9]+";
20029
19986
  const v7_keywords = "далее ";
@@ -70669,6 +70626,252 @@ class HastEmitter {
70669
70626
  return "";
70670
70627
  }
70671
70628
  }
70629
+ const popoverVariants = cva(
70630
+ "z-[999999] w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
70631
+ );
70632
+ const inputVariants = cva(
70633
+ "flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:ring-transparent focus-visible:outline-none md:text-sm"
70634
+ );
70635
+ function LinkFloatingToolbar({
70636
+ state
70637
+ }) {
70638
+ const activeCommentId = usePluginOption({ key: "comment" }, "activeId");
70639
+ const activeSuggestionId = usePluginOption({ key: "suggestion" }, "activeId");
70640
+ const { api, editor } = useEditorPlugin(LinkPlugin);
70641
+ const isUrlValidator = usePluginOption(LinkPlugin, "isUrl");
70642
+ const [currentUrl, setCurrentUrl] = React.useState("");
70643
+ const [isValidUrl, setIsValidUrl] = React.useState(true);
70644
+ const handleUrlInput = React.useCallback(
70645
+ (e) => {
70646
+ const value = e.currentTarget.value;
70647
+ setCurrentUrl(value);
70648
+ if (value && isUrlValidator) {
70649
+ const valid = isUrlValidator(value);
70650
+ setIsValidUrl(valid);
70651
+ } else {
70652
+ setIsValidUrl(true);
70653
+ }
70654
+ },
70655
+ [isUrlValidator]
70656
+ );
70657
+ const floatingOptions = React.useMemo(() => {
70658
+ return {
70659
+ middleware: [
70660
+ offset(8),
70661
+ flip({
70662
+ fallbackPlacements: ["bottom-end", "top-start", "top-end"],
70663
+ padding: 12
70664
+ }),
70665
+ shift({ padding: 8, limiter: limitShift() })
70666
+ ],
70667
+ placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start"
70668
+ };
70669
+ }, [activeCommentId, activeSuggestionId]);
70670
+ const insertState = useFloatingLinkInsertState({
70671
+ ...state,
70672
+ floatingOptions: {
70673
+ ...floatingOptions,
70674
+ ...state == null ? void 0 : state.floatingOptions
70675
+ }
70676
+ });
70677
+ const {
70678
+ hidden,
70679
+ props: insertProps,
70680
+ ref: insertRef,
70681
+ textInputProps
70682
+ } = useFloatingLinkInsert(insertState);
70683
+ const editState = useFloatingLinkEditState({
70684
+ ...state,
70685
+ floatingOptions: {
70686
+ ...floatingOptions,
70687
+ ...state == null ? void 0 : state.floatingOptions
70688
+ }
70689
+ });
70690
+ const {
70691
+ editButtonProps,
70692
+ props: editProps,
70693
+ ref: editRef,
70694
+ unlinkButtonProps
70695
+ } = useFloatingLinkEdit(editState);
70696
+ const inputProps = useFormInputProps({
70697
+ preventDefaultOnEnterKeydown: true
70698
+ });
70699
+ if (hidden) return null;
70700
+ const input = /* @__PURE__ */ React.createElement(
70701
+ "div",
70702
+ {
70703
+ className: "z-[999999] flex w-[330px] flex-col relative",
70704
+ ...inputProps
70705
+ },
70706
+ !isValidUrl && currentUrl && /* @__PURE__ */ React.createElement("div", { className: "absolute -top-16 left-0 right-0 z-[1000000] mb-2" }, /* @__PURE__ */ React.createElement(
70707
+ "div",
70708
+ {
70709
+ className: "bg-red-50 border border-red-200 rounded-md p-2 shadow-lg",
70710
+ role: "alert",
70711
+ "aria-live": "polite"
70712
+ },
70713
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement(CircleX, { className: "size-4 text-red-500 mr-2 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-red-700 text-wrap" }, "Invalid URL. Please prefix link with https:// or use a relative path like /about")),
70714
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -bottom-1 left-4 w-2 h-2 bg-red-50 border-r border-b border-red-200 transform rotate-45" })
70715
+ )),
70716
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Link, { className: "size-4" })), /* @__PURE__ */ React.createElement(
70717
+ FloatingLinkUrlInput,
70718
+ {
70719
+ className: inputVariants(),
70720
+ placeholder: "Paste link",
70721
+ "data-plate-focus": true,
70722
+ onInput: handleUrlInput
70723
+ }
70724
+ )),
70725
+ /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
70726
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Text, { className: "size-4" })), /* @__PURE__ */ React.createElement(
70727
+ "input",
70728
+ {
70729
+ className: inputVariants(),
70730
+ placeholder: "Text to display",
70731
+ "data-plate-focus": true,
70732
+ ...textInputProps
70733
+ }
70734
+ )),
70735
+ /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
70736
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-2 px-2 py-1" }, /* @__PURE__ */ React.createElement(
70737
+ "button",
70738
+ {
70739
+ type: "button",
70740
+ className: buttonVariants$2({ size: "sm", variant: "ghost" }),
70741
+ onClick: () => {
70742
+ api.floatingLink.hide();
70743
+ }
70744
+ },
70745
+ "Cancel"
70746
+ ), /* @__PURE__ */ React.createElement(
70747
+ "button",
70748
+ {
70749
+ type: "button",
70750
+ className: buttonVariants$2({
70751
+ size: "sm",
70752
+ variant: "tinaPrimary"
70753
+ }),
70754
+ onClick: () => {
70755
+ if (isValidUrl && currentUrl) {
70756
+ submitFloatingLink(editor);
70757
+ }
70758
+ },
70759
+ disabled: !isValidUrl && !!currentUrl
70760
+ },
70761
+ "OK"
70762
+ ))
70763
+ );
70764
+ const editContent = editState.isEditing ? input : /* @__PURE__ */ React.createElement("div", { className: "box-content flex items-center" }, /* @__PURE__ */ React.createElement(
70765
+ "button",
70766
+ {
70767
+ className: buttonVariants$2({ size: "sm", variant: "ghost" }),
70768
+ type: "button",
70769
+ ...editButtonProps
70770
+ },
70771
+ "Edit link"
70772
+ ), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(LinkOpenButton, null), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(
70773
+ "button",
70774
+ {
70775
+ className: buttonVariants$2({
70776
+ size: "icon",
70777
+ variant: "ghost"
70778
+ }),
70779
+ type: "button",
70780
+ ...unlinkButtonProps
70781
+ },
70782
+ /* @__PURE__ */ React.createElement(Unlink, { width: 18 })
70783
+ ));
70784
+ return /* @__PURE__ */ React.createElement(PortalBody, null, /* @__PURE__ */ React.createElement(
70785
+ "div",
70786
+ {
70787
+ ref: insertRef,
70788
+ className: popoverVariants(),
70789
+ ...insertProps,
70790
+ style: {
70791
+ ...insertProps.style,
70792
+ zIndex: 999999
70793
+ }
70794
+ },
70795
+ input
70796
+ ), /* @__PURE__ */ React.createElement(
70797
+ "div",
70798
+ {
70799
+ ref: editRef,
70800
+ className: popoverVariants(),
70801
+ ...editProps,
70802
+ style: { ...editProps.style, zIndex: 999999 }
70803
+ },
70804
+ editContent
70805
+ ));
70806
+ }
70807
+ function LinkOpenButton() {
70808
+ const editor = useEditorRef();
70809
+ const selection = useEditorSelection();
70810
+ const attributes = React.useMemo(
70811
+ () => {
70812
+ const entry = editor.api.node({
70813
+ match: { type: editor.getType(LinkPlugin) }
70814
+ });
70815
+ if (!entry) {
70816
+ return {};
70817
+ }
70818
+ const [element] = entry;
70819
+ return getLinkAttributes(editor, element);
70820
+ },
70821
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70822
+ [editor, selection]
70823
+ );
70824
+ return /* @__PURE__ */ React.createElement(
70825
+ "a",
70826
+ {
70827
+ ...attributes,
70828
+ className: buttonVariants$2({
70829
+ size: "icon",
70830
+ variant: "ghost"
70831
+ }),
70832
+ onMouseOver: (e) => {
70833
+ e.stopPropagation();
70834
+ },
70835
+ "aria-label": "Open link in a new tab",
70836
+ target: "_blank"
70837
+ },
70838
+ /* @__PURE__ */ React.createElement(ExternalLink, { width: 18 })
70839
+ );
70840
+ }
70841
+ const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
70842
+ const emailLintRE = /mailto:([^?\\]+)/;
70843
+ const telLintRE = /tel:([\d-]+)/;
70844
+ const localhostDomainRE = /^localhost[\d:?]*(?:[^\d:?]\S*)?$/;
70845
+ const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/;
70846
+ const localUrlRE = /^\/\S+/;
70847
+ const isUrl = (string) => {
70848
+ if (typeof string !== "string") {
70849
+ return false;
70850
+ }
70851
+ if (string.startsWith("#")) {
70852
+ return true;
70853
+ }
70854
+ const generalMatch = string.match(protocolAndDomainRE);
70855
+ const emailLinkMatch = string.match(emailLintRE);
70856
+ const telLinkMatch = string.match(telLintRE);
70857
+ const localUrlMatch = string.match(localUrlRE);
70858
+ if (emailLinkMatch || telLinkMatch || localUrlMatch) {
70859
+ return true;
70860
+ }
70861
+ if (generalMatch) {
70862
+ const everythingAfterProtocol = generalMatch[1];
70863
+ if (!everythingAfterProtocol) {
70864
+ return false;
70865
+ }
70866
+ try {
70867
+ new URL(string);
70868
+ } catch {
70869
+ return false;
70870
+ }
70871
+ return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol);
70872
+ }
70873
+ return false;
70874
+ };
70672
70875
  const createHTMLBlockPlugin = createPlatePlugin({
70673
70876
  key: "html",
70674
70877
  node: {
@@ -70685,18 +70888,26 @@ const createHTMLInlinePlugin = createPlatePlugin({
70685
70888
  isInline: true
70686
70889
  }
70687
70890
  });
70688
- const KEY_BLOCKQUOTE_ENTER_BREAK = "blockquote-enter-break";
70689
- const createBlockquoteEnterBreakPlugin = createPlatePlugin({
70690
- key: KEY_BLOCKQUOTE_ENTER_BREAK,
70891
+ const KEY_HARD_BREAK = "tina-hard-break";
70892
+ const NO_HARD_BREAK = [
70893
+ CodeBlockPlugin.key,
70894
+ TableCellPlugin.key,
70895
+ TableCellHeaderPlugin.key
70896
+ ];
70897
+ const createHardBreakPlugin = createPlatePlugin({
70898
+ key: KEY_HARD_BREAK,
70691
70899
  handlers: {
70692
70900
  onKeyDown: ({ editor, event }) => {
70693
70901
  var _a;
70694
- if (event.key !== "Enter") return;
70695
- const blockquoteEntry = editor.api.above({
70902
+ if (event.key !== "Enter" || event.metaKey || event.ctrlKey) return;
70903
+ const inBlockquote = editor.api.above({
70696
70904
  match: { type: BlockquotePlugin.key }
70697
70905
  });
70698
- if (!blockquoteEntry) return;
70906
+ if (!event.shiftKey && !inBlockquote) return;
70907
+ if (editor.api.some({ match: { type: NO_HARD_BREAK } })) return;
70908
+ if (!editor.selection) return;
70699
70909
  event.preventDefault();
70910
+ if (editor.api.isExpanded()) editor.tf.delete();
70700
70911
  const cursorPosition = (_a = editor.selection) == null ? void 0 : _a.focus;
70701
70912
  if (!cursorPosition) return;
70702
70913
  editor.tf.insertNodes(
@@ -70724,6 +70935,303 @@ const createBreakPlugin = createPlatePlugin({
70724
70935
  }
70725
70936
  }
70726
70937
  });
70938
+ const buildError = (element) => {
70939
+ return {
70940
+ message: element.message,
70941
+ position: element.position && {
70942
+ endColumn: element.position.end.column,
70943
+ startColumn: element.position.start.column,
70944
+ startLineNumber: element.position.start.line,
70945
+ endLineNumber: element.position.end.line
70946
+ }
70947
+ };
70948
+ };
70949
+ const buildErrorMessage = (element) => {
70950
+ if (!element) {
70951
+ return "";
70952
+ }
70953
+ const errorMessage = buildError(element);
70954
+ const message = errorMessage ? `${errorMessage.message}${errorMessage.position ? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}` : ""}` : null;
70955
+ return message;
70956
+ };
70957
+ const ELEMENT_INVALID_MARKDOWN = "invalid_markdown";
70958
+ const createInvalidMarkdownPlugin = createPlatePlugin({
70959
+ key: ELEMENT_INVALID_MARKDOWN,
70960
+ options: {
70961
+ isElement: true,
70962
+ isVoid: true,
70963
+ isInline: false
70964
+ },
70965
+ node: {
70966
+ component: InvalidMarkdownElement
70967
+ }
70968
+ });
70969
+ function InvalidMarkdownElement({
70970
+ attributes,
70971
+ element,
70972
+ children
70973
+ }) {
70974
+ return /* @__PURE__ */ React__default.createElement("div", { ...attributes }, /* @__PURE__ */ React__default.createElement(ErrorMessage, { error: element }), children);
70975
+ }
70976
+ function ErrorMessage({ error }) {
70977
+ const message = buildErrorMessage(error);
70978
+ const { setRawMode } = useEditorContext();
70979
+ return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "bg-red-50 sm:rounded-lg" }, /* @__PURE__ */ React__default.createElement("div", { className: "px-4 py-5 sm:p-6" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg leading-6 font-medium text-red-800" }, "❌ Error parsing markdown"), /* @__PURE__ */ React__default.createElement("div", { className: "mt-2 max-w-xl text-sm text-red-800 space-y-4" }, /* @__PURE__ */ React__default.createElement("p", null, message), /* @__PURE__ */ React__default.createElement("p", null, "To fix these errors, edit the content in raw-mode."), /* @__PURE__ */ React__default.createElement(
70980
+ "button",
70981
+ {
70982
+ type: "button",
70983
+ onClick: () => setRawMode(true),
70984
+ className: "rounded-l border-r-0 shadow rounded bg-white cursor-pointer relative inline-flex items-center px-2 py-2 border border-gray-200 hover:text-white text-sm font-medium transition-all ease-out duration-150 hover:bg-gray-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"
70985
+ },
70986
+ "Switch to raw-mode"
70987
+ ))));
70988
+ }
70989
+ const useCustomFloatingToolbarState = ({
70990
+ editorId,
70991
+ floatingOptions,
70992
+ focusedEditorId,
70993
+ hideToolbar,
70994
+ showWhenReadOnly
70995
+ }) => {
70996
+ const editor = useEditorRef();
70997
+ const selectionExpanded = useEditorSelector(
70998
+ () => editor.api.isExpanded(),
70999
+ []
71000
+ );
71001
+ const selectionText = useEditorSelector(() => editor.api.string(), []);
71002
+ const readOnly = useEditorReadOnly();
71003
+ const focused = useFocused();
71004
+ const [open, setOpen] = React__default.useState(false);
71005
+ const [waitForCollapsedSelection, setWaitForCollapsedSelection] = React__default.useState(false);
71006
+ const [mousedown, setMousedown] = React__default.useState(false);
71007
+ const floating = useVirtualFloating(
71008
+ mergeProps(
71009
+ {
71010
+ open,
71011
+ getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
71012
+ onOpenChange: setOpen
71013
+ },
71014
+ floatingOptions
71015
+ )
71016
+ );
71017
+ return {
71018
+ editorId,
71019
+ floating,
71020
+ focused,
71021
+ focusedEditorId,
71022
+ hideToolbar,
71023
+ mousedown,
71024
+ open,
71025
+ readOnly,
71026
+ selectionExpanded,
71027
+ selectionText,
71028
+ setMousedown,
71029
+ setOpen,
71030
+ setWaitForCollapsedSelection,
71031
+ showWhenReadOnly,
71032
+ waitForCollapsedSelection
71033
+ };
71034
+ };
71035
+ const useCustomFloatingToolbar = ({
71036
+ editorId,
71037
+ floating,
71038
+ focusedEditorId,
71039
+ hideToolbar,
71040
+ mousedown,
71041
+ open,
71042
+ readOnly,
71043
+ selectionExpanded,
71044
+ selectionText,
71045
+ setMousedown,
71046
+ setOpen,
71047
+ setWaitForCollapsedSelection,
71048
+ showWhenReadOnly,
71049
+ waitForCollapsedSelection
71050
+ }) => {
71051
+ React__default.useEffect(() => {
71052
+ if (!(editorId === focusedEditorId)) {
71053
+ setWaitForCollapsedSelection(true);
71054
+ }
71055
+ if (!selectionExpanded) {
71056
+ setWaitForCollapsedSelection(false);
71057
+ }
71058
+ }, [
71059
+ editorId,
71060
+ focusedEditorId,
71061
+ selectionExpanded,
71062
+ setWaitForCollapsedSelection
71063
+ ]);
71064
+ React__default.useEffect(() => {
71065
+ const mouseup = () => setMousedown(false);
71066
+ const mousedown2 = () => setMousedown(true);
71067
+ document.addEventListener("mouseup", mouseup);
71068
+ document.addEventListener("mousedown", mousedown2);
71069
+ return () => {
71070
+ document.removeEventListener("mouseup", mouseup);
71071
+ document.removeEventListener("mousedown", mousedown2);
71072
+ };
71073
+ }, []);
71074
+ React__default.useEffect(() => {
71075
+ if (!selectionExpanded || !selectionText || mousedown && !open || hideToolbar || readOnly && !showWhenReadOnly) {
71076
+ setOpen(false);
71077
+ } else if (selectionText && selectionExpanded && (!waitForCollapsedSelection || readOnly)) {
71078
+ setOpen(true);
71079
+ }
71080
+ }, [
71081
+ setOpen,
71082
+ editorId,
71083
+ focusedEditorId,
71084
+ hideToolbar,
71085
+ showWhenReadOnly,
71086
+ selectionExpanded,
71087
+ selectionText,
71088
+ mousedown,
71089
+ waitForCollapsedSelection,
71090
+ open,
71091
+ readOnly
71092
+ ]);
71093
+ const { update } = floating;
71094
+ useEditorSelector(() => {
71095
+ update == null ? void 0 : update();
71096
+ }, [update]);
71097
+ const clickOutsideRef = useOnClickOutside(
71098
+ () => {
71099
+ setOpen(false);
71100
+ },
71101
+ {
71102
+ ignoreClass: "ignore-click-outside/toolbar"
71103
+ }
71104
+ );
71105
+ return {
71106
+ clickOutsideRef,
71107
+ hidden: !open,
71108
+ props: {
71109
+ style: floating.style
71110
+ },
71111
+ ref: floating.refs.setFloating
71112
+ };
71113
+ };
71114
+ const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
71115
+ const editorId = useEditorRef();
71116
+ const focusedEditorId = useEventEditorValue("focus");
71117
+ const test = useCustomFloatingToolbarState({
71118
+ editorId: editorId.id,
71119
+ focusedEditorId,
71120
+ ...state,
71121
+ floatingOptions: {
71122
+ middleware: [
71123
+ offset(12),
71124
+ flip({
71125
+ fallbackPlacements: [
71126
+ "top-start",
71127
+ "top-end",
71128
+ "bottom-start",
71129
+ "bottom-end"
71130
+ ],
71131
+ padding: 12
71132
+ })
71133
+ ],
71134
+ placement: "top",
71135
+ ...state == null ? void 0 : state.floatingOptions
71136
+ }
71137
+ });
71138
+ const {
71139
+ hidden,
71140
+ props: rootProps,
71141
+ ref: floatingRef
71142
+ } = useCustomFloatingToolbar(test);
71143
+ const ref = useComposedRef$1(propRef, floatingRef);
71144
+ if (hidden) return null;
71145
+ return /* @__PURE__ */ React__default.createElement(PortalBody, null, /* @__PURE__ */ React__default.createElement(
71146
+ Toolbar,
71147
+ {
71148
+ className: cn$1(
71149
+ "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
71150
+ ),
71151
+ ...props,
71152
+ ...rootProps,
71153
+ ref
71154
+ },
71155
+ children
71156
+ ));
71157
+ });
71158
+ function TurnIntoDropdownMenu(props) {
71159
+ const { headingLevels } = useToolbarContext();
71160
+ const items = [
71161
+ paragraphItem,
71162
+ ...headingLevels.map((level) => headingItemsByLevel[level])
71163
+ ];
71164
+ const value = useEditorSelector((editor2) => {
71165
+ let initialNodeType = ParagraphPlugin.key;
71166
+ let allNodesMatchInitialNodeType = false;
71167
+ const codeBlockEntries = editor2.api.nodes({
71168
+ match: (n) => editor2.api.isBlock(n),
71169
+ mode: "highest"
71170
+ });
71171
+ const nodes = Array.from(codeBlockEntries);
71172
+ if (nodes.length > 0) {
71173
+ initialNodeType = nodes[0][0].type;
71174
+ allNodesMatchInitialNodeType = nodes.every(([node]) => {
71175
+ const type2 = (node == null ? void 0 : node.type) || ParagraphPlugin.key;
71176
+ return type2 === initialNodeType;
71177
+ });
71178
+ }
71179
+ return allNodesMatchInitialNodeType ? initialNodeType : ParagraphPlugin.key;
71180
+ }, []);
71181
+ const editor = useEditorRef();
71182
+ const openState = useOpenState();
71183
+ const selectedItem = getHeadingItem(value) ?? paragraphItem;
71184
+ const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
71185
+ const editorState = useEditorState();
71186
+ const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
71187
+ if (userInTable) return null;
71188
+ return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
71189
+ ToolbarButton,
71190
+ {
71191
+ className: "lg:min-w-[130px]",
71192
+ isDropdown: true,
71193
+ showArrow: true,
71194
+ pressed: openState.open,
71195
+ tooltip: "Turn into"
71196
+ },
71197
+ /* @__PURE__ */ React__default.createElement("span", { className: "" }, selectedItemLabel)
71198
+ )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-0" }, /* @__PURE__ */ React__default.createElement(DropdownMenuLabel$1, null, "Turn into"), /* @__PURE__ */ React__default.createElement(
71199
+ DropdownMenuRadioGroup$1,
71200
+ {
71201
+ className: "flex flex-col gap-0.5",
71202
+ onValueChange: (type2) => {
71203
+ if (type2 === "ul" || type2 === "ol") {
71204
+ toggleList(editor, { type: type2 });
71205
+ } else {
71206
+ unwrapList(editor);
71207
+ editor.tf.toggleBlock(type2);
71208
+ }
71209
+ editor.tf.collapse();
71210
+ editor.tf.focus();
71211
+ },
71212
+ value
71213
+ },
71214
+ items.map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
71215
+ DropdownMenuRadioItem$1,
71216
+ {
71217
+ className: "min-w-[180px]",
71218
+ key: itemValue,
71219
+ value: itemValue
71220
+ },
71221
+ /* @__PURE__ */ React__default.createElement(Icon, { className: "mr-2 size-5" }),
71222
+ label
71223
+ ))
71224
+ )));
71225
+ }
71226
+ function FloatingToolbarButtons() {
71227
+ return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(TurnIntoDropdownMenu, null));
71228
+ }
71229
+ const FloatingToolbarPlugin = createPlatePlugin({
71230
+ key: "floating-toolbar",
71231
+ render: {
71232
+ afterEditable: () => /* @__PURE__ */ React__default.createElement(FloatingToolbar, null, /* @__PURE__ */ React__default.createElement(FloatingToolbarButtons, null))
71233
+ }
71234
+ });
70727
71235
  [
70728
71236
  HEADING_KEYS.h1,
70729
71237
  HEADING_KEYS.h2,
@@ -70788,7 +71296,7 @@ const createEditorPlugins = ({
70788
71296
  createImgPlugin,
70789
71297
  createHTMLBlockPlugin,
70790
71298
  createHTMLInlinePlugin,
70791
- createBlockquoteEnterBreakPlugin,
71299
+ createHardBreakPlugin,
70792
71300
  createInvalidMarkdownPlugin,
70793
71301
  CorrectNodeBehaviorPlugin,
70794
71302
  ClearHighlightOnEnterPlugin,
@@ -70887,6 +71395,8 @@ const createEditorPlugins = ({
70887
71395
  ]
70888
71396
  }
70889
71397
  }),
71398
+ // Only reached where `createHardBreakPlugin` bails (code blocks, table
71399
+ // cells) — everywhere else it has already inserted a `break` element.
70890
71400
  SoftBreakPlugin.configure({
70891
71401
  options: {
70892
71402
  rules: [
@@ -71067,6 +71577,7 @@ const MediaWorkflowOverlay = () => {
71067
71577
  phase: "confirming",
71068
71578
  branchName: event.branchName,
71069
71579
  baseBranch: event.baseBranch,
71580
+ allowSaveToProtectedBranch: event.allowSaveToProtectedBranch,
71070
71581
  onConfirm: event.onConfirm,
71071
71582
  onCancel: event.onCancel,
71072
71583
  onSaveToProtectedBranch: event.onSaveToProtectedBranch
@@ -71201,6 +71712,7 @@ const MediaWorkflowOverlay = () => {
71201
71712
  );
71202
71713
  },
71203
71714
  onCreateBranch: handleCreateBranch,
71715
+ allowSaveToProtectedBranch: state.allowSaveToProtectedBranch,
71204
71716
  onSaveToProtectedBranch: () => {
71205
71717
  abortPreflight();
71206
71718
  state.onSaveToProtectedBranch();
@@ -71390,6 +71902,14 @@ const DefaultSessionProvider = ({
71390
71902
  children
71391
71903
  }) => /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, children);
71392
71904
  class AbstractAuthProvider {
71905
+ constructor() {
71906
+ /**
71907
+ * Turns a tokened 401 into the auth wall re-arming. The Client assigns this
71908
+ * automatically; AbstractAuthProvider.fetchWithToken invokes it. A provider
71909
+ * that implements its own fetchWithToken must invoke it itself.
71910
+ */
71911
+ __publicField(this, "sessionExpiredListener");
71912
+ }
71393
71913
  /**
71394
71914
  * Wraps the normal fetch function with same API but adds the authorization header token.
71395
71915
  *
@@ -71400,15 +71920,20 @@ class AbstractAuthProvider {
71400
71920
  * @param init fetch function init
71401
71921
  */
71402
71922
  async fetchWithToken(input, init) {
71923
+ var _a;
71403
71924
  const headers = (init == null ? void 0 : init.headers) || {};
71404
71925
  const accessToken = await this.getAccessToken();
71405
71926
  if (accessToken) {
71406
71927
  headers["Authorization"] = "Bearer " + accessToken;
71407
71928
  }
71408
- return await fetch(input, {
71929
+ const res = await fetch(input, {
71409
71930
  ...init || {},
71410
71931
  headers: new Headers(headers)
71411
71932
  });
71933
+ if (res.status === 401 && accessToken) {
71934
+ (_a = this.sessionExpiredListener) == null ? void 0 : _a.call(this);
71935
+ }
71936
+ return res;
71412
71937
  }
71413
71938
  async getAccessToken() {
71414
71939
  const token = await this.getToken();
@@ -71529,9 +72054,14 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71529
72054
  }
71530
72055
  return null;
71531
72056
  }
71532
- const res = await this.fetchWithToken(url, {
71533
- method: "GET"
71534
- });
72057
+ let res;
72058
+ try {
72059
+ res = await this.fetchWithToken(url, { method: "GET" });
72060
+ } catch (networkError) {
72061
+ console.warn("TinaCMS: session check failed, retrying.", networkError);
72062
+ await new Promise((resolve) => setTimeout(resolve, 300));
72063
+ res = await this.fetchWithToken(url, { method: "GET" });
72064
+ }
71535
72065
  const val = await res.json();
71536
72066
  if (!res.status.toString().startsWith("2")) {
71537
72067
  console.error(
@@ -71542,6 +72072,9 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71542
72072
  }
71543
72073
  return val;
71544
72074
  } catch (e) {
72075
+ if (e instanceof TypeError || isErrorNamed(e, "AbortError")) {
72076
+ throw e;
72077
+ }
71545
72078
  console.error(e);
71546
72079
  return null;
71547
72080
  }
@@ -71658,6 +72191,8 @@ class Client {
71658
72191
  // automatically hooked into global event bus when attached via cms.
71659
72192
  __publicField(this, "protectedBranches", []);
71660
72193
  __publicField(this, "usingEditorialWorkflow", false);
72194
+ // Whatever the auth provider returned from getUser().
72195
+ __publicField(this, "user");
71661
72196
  __publicField(this, "addPendingContent", async (props) => {
71662
72197
  const mutation = `#graphql
71663
72198
  mutation addPendingDocumentMutation(
@@ -71757,13 +72292,24 @@ mutation addPendingDocumentMutation(
71757
72292
  }
71758
72293
  );
71759
72294
  this.clientId = options.clientId;
71760
- this.authProvider = ((_u = (_t = (_s = this.schema) == null ? void 0 : _s.config) == null ? void 0 : _t.config) == null ? void 0 : _u.authProvider) || new TinaCloudAuthProvider({
71761
- clientId: options.clientId,
71762
- identityApiUrl: this.identityApiUrl,
71763
- getTokenFn: options.getTokenFn,
71764
- tokenStorage,
71765
- frontendUrl: this.frontendUrl
71766
- });
72295
+ this.authProvider = this.adoptAuthProvider(
72296
+ ((_u = (_t = (_s = this.schema) == null ? void 0 : _s.config) == null ? void 0 : _t.config) == null ? void 0 : _u.authProvider) || new TinaCloudAuthProvider({
72297
+ clientId: options.clientId,
72298
+ identityApiUrl: this.identityApiUrl,
72299
+ getTokenFn: options.getTokenFn,
72300
+ tokenStorage,
72301
+ frontendUrl: this.frontendUrl
72302
+ })
72303
+ );
72304
+ }
72305
+ /**
72306
+ * Every provider swap must come through here: the listener is what turns a
72307
+ * REST 401 into the auth wall re-arming, and a bare field assignment would
72308
+ * silently drop it.
72309
+ */
72310
+ adoptAuthProvider(provider) {
72311
+ provider.sessionExpiredListener = () => dispatchSessionExpired(this.events);
72312
+ return provider;
71767
72313
  }
71768
72314
  get isLocalMode() {
71769
72315
  return false;
@@ -71808,6 +72354,17 @@ mutation addPendingDocumentMutation(
71808
72354
  variables
71809
72355
  })
71810
72356
  });
72357
+ if (res.status === 401) {
72358
+ res.json().catch(() => {
72359
+ });
72360
+ if (this.isCustomContentApi) {
72361
+ console.error(
72362
+ `TinaCMS: the content API at ${this.contentApiUrl} returned 401. If signing in again does not resolve this, check the backend's auth configuration (clientId: ${this.options.clientId}, branch: ${this.branch}).`
72363
+ );
72364
+ }
72365
+ dispatchSessionExpired(this.events);
72366
+ throw new SessionExpiredError();
72367
+ }
71811
72368
  if (res.status !== 200) {
71812
72369
  let errorMessage = `Unable to complete request, ${res.statusText}`;
71813
72370
  const resBody = await res.json();
@@ -72229,6 +72786,10 @@ mutation addPendingDocumentMutation(
72229
72786
  throw error;
72230
72787
  }
72231
72788
  }
72789
+ /**
72790
+ * The server normalises `branchName`, so follow-up calls must use the
72791
+ * `branchName` from the response rather than the one they sent.
72792
+ */
72232
72793
  async startMediaEditorialWorkflow(options) {
72233
72794
  const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}/media`;
72234
72795
  return await this.postEditorialWorkflow(
@@ -72253,7 +72814,9 @@ class LocalClient extends Client {
72253
72814
  customContentApiUrl: props && props.customContentApiUrl ? props.customContentApiUrl : DEFAULT_LOCAL_TINA_GQL_SERVER_URL
72254
72815
  };
72255
72816
  super(clientProps);
72256
- this.authProvider = ((_c = (_b = (_a = this.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.config) == null ? void 0 : _c.authProvider) || new LocalAuthProvider();
72817
+ this.authProvider = this.adoptAuthProvider(
72818
+ ((_c = (_b = (_a = this.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.config) == null ? void 0 : _c.authProvider) || new LocalAuthProvider()
72819
+ );
72257
72820
  }
72258
72821
  get isLocalMode() {
72259
72822
  return true;
@@ -72544,6 +73107,7 @@ const AuthWallInner = ({
72544
73107
  if (!mounted) return;
72545
73108
  if (isAuthorized) {
72546
73109
  const user = await client.authProvider.getUser();
73110
+ client.user = user;
72547
73111
  if (user.passwordChangeRequired) {
72548
73112
  window.location.hash = "#/screens/change_password";
72549
73113
  }
@@ -72599,7 +73163,7 @@ const AuthWallInner = ({
72599
73163
  }
72600
73164
  return onAuthenticated();
72601
73165
  } catch (e) {
72602
- if (e instanceof AuthenticationCancelledError || (e == null ? void 0 : e.name) === "AuthenticationCancelledError") {
73166
+ if (e instanceof AuthenticationCancelledError || isErrorNamed(e, "AuthenticationCancelledError")) {
72603
73167
  return;
72604
73168
  }
72605
73169
  console.error(e);
@@ -73948,6 +74512,7 @@ const CollectionListPage = () => {
73948
74512
  );
73949
74513
  return (_b2 = (_a2 = doc == null ? void 0 : doc.document) == null ? void 0 : _a2._sys) == null ? void 0 : _b2.hasReferences;
73950
74514
  } catch (error) {
74515
+ if (isSessionExpiredError(error)) throw error;
73951
74516
  cms.alerts.error(
73952
74517
  "Document was not found, ask a developer for help or check the console for an error message"
73953
74518
  );
@@ -73963,6 +74528,7 @@ const CollectionListPage = () => {
73963
74528
  );
73964
74529
  reFetchCollection();
73965
74530
  } catch (error) {
74531
+ if (isSessionExpiredError(error)) throw error;
73966
74532
  if (error.message.includes(ERR_HAS_REFERENCES)) {
73967
74533
  cms.alerts.error(
73968
74534
  error.message.split("\n ").filter(Boolean)[1]
@@ -73992,6 +74558,7 @@ const CollectionListPage = () => {
73992
74558
  );
73993
74559
  reFetchCollection();
73994
74560
  } catch (error) {
74561
+ if (isSessionExpiredError(error)) throw error;
73995
74562
  cms.alerts.warn(
73996
74563
  "Document was not deleted, ask a developer for help or check the console for an error message"
73997
74564
  );
@@ -74024,6 +74591,7 @@ const CollectionListPage = () => {
74024
74591
  );
74025
74592
  reFetchCollection();
74026
74593
  } catch (error) {
74594
+ if (isSessionExpiredError(error)) throw error;
74027
74595
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
74028
74596
  cms.alerts.error(
74029
74597
  `Document was not renamed. The filename "${vars.newRelativePath}" is already used by another document, please choose a different name.`
@@ -74070,6 +74638,7 @@ const CollectionListPage = () => {
74070
74638
  );
74071
74639
  cms.alerts.info("Folder was successfully created");
74072
74640
  } catch (error) {
74641
+ if (isSessionExpiredError(error)) throw error;
74073
74642
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
74074
74643
  cms.alerts.error(
74075
74644
  `Folder was not created, folder with name "${vars.folderName}" already exists ${folder.name ? `in ${folder.name}` : ""}`
@@ -74633,6 +75202,7 @@ const DeleteModal = ({
74633
75202
  useEffect(() => {
74634
75203
  checkRefsFunc().then((result) => {
74635
75204
  setHasRefs(result);
75205
+ }).catch(() => {
74636
75206
  });
74637
75207
  }, [filename, checkRefsFunc]);
74638
75208
  return /* @__PURE__ */ React__default.createElement(Modal, null, /* @__PURE__ */ React__default.createElement(PopupModal, null, /* @__PURE__ */ React__default.createElement(ModalHeader, { close: close2 }, "Delete ", filename), /* @__PURE__ */ React__default.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React__default.createElement("p", null, `Are you sure you want to delete ${filename}?${hasRefs ? " References to this document will also be deleted." : ""}`)), /* @__PURE__ */ React__default.createElement(ModalActions, null, /* @__PURE__ */ React__default.createElement(Button$2, { style: { flexGrow: 2 }, onClick: close2 }, "Cancel"), /* @__PURE__ */ React__default.createElement(
@@ -74641,7 +75211,10 @@ const DeleteModal = ({
74641
75211
  style: { flexGrow: 3 },
74642
75212
  variant: "danger",
74643
75213
  onClick: async () => {
74644
- await deleteFunc();
75214
+ try {
75215
+ await deleteFunc();
75216
+ } catch {
75217
+ }
74645
75218
  close2();
74646
75219
  }
74647
75220
  },
@@ -74849,16 +75422,18 @@ const useGetCollection = (cms, collectionName, includeDocuments = true, folder,
74849
75422
  );
74850
75423
  setCollection(collection2);
74851
75424
  } else if (!cancelled) {
74852
- cms.events.dispatch({ type: "cms:session-expired" });
75425
+ dispatchSessionExpired(cms.events);
74853
75426
  setCollection(void 0);
74854
75427
  }
74855
75428
  } catch (error2) {
74856
- cms.alerts.error(
74857
- `[${error2.name}] GetCollection failed: ${error2.message}`
74858
- );
74859
- console.error(error2);
74860
75429
  setCollection(void 0);
74861
- setError(error2);
75430
+ if (!isSessionExpiredError(error2)) {
75431
+ cms.alerts.error(
75432
+ `[${error2.name}] GetCollection failed: ${error2.message}`
75433
+ );
75434
+ console.error(error2);
75435
+ setError(error2);
75436
+ }
74862
75437
  }
74863
75438
  if (!cancelled) {
74864
75439
  setLoading(false);
@@ -74910,6 +75485,15 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74910
75485
  return api.fetchDocument(collection2, relativePath, false);
74911
75486
  })
74912
75487
  );
75488
+ if (docs.some(
75489
+ (p) => p.status === "rejected" && isSessionExpiredError(p.reason)
75490
+ )) {
75491
+ setCollection(void 0);
75492
+ if (!cancelled) {
75493
+ setLoading(false);
75494
+ }
75495
+ return;
75496
+ }
74913
75497
  const edges = docs.filter((p) => {
74914
75498
  var _a;
74915
75499
  return p.status === "fulfilled" && !!((_a = p.value) == null ? void 0 : _a.document);
@@ -74932,16 +75516,18 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74932
75516
  };
74933
75517
  setCollection(collectionData);
74934
75518
  } else if (!cancelled) {
74935
- cms.events.dispatch({ type: "cms:session-expired" });
75519
+ dispatchSessionExpired(cms.events);
74936
75520
  setCollection(void 0);
74937
75521
  }
74938
75522
  } catch (error2) {
74939
- cms.alerts.error(
74940
- `[${error2.name}] GetCollection failed: ${error2.message}`
74941
- );
74942
- console.error(error2);
74943
75523
  setCollection(void 0);
74944
- setError(error2);
75524
+ if (!isSessionExpiredError(error2)) {
75525
+ cms.alerts.error(
75526
+ `[${error2.name}] GetCollection failed: ${error2.message}`
75527
+ );
75528
+ console.error(error2);
75529
+ setError(error2);
75530
+ }
74945
75531
  }
74946
75532
  if (!cancelled) {
74947
75533
  setLoading(false);
@@ -75051,9 +75637,7 @@ const createDocument = async (cms, collection, template, mutationInfo, folder, v
75051
75637
  if (await api.isAuthenticated()) {
75052
75638
  await api.createDocument(collection, relativePath, params);
75053
75639
  } else {
75054
- const authMessage = `CreateDocument failed: User is no longer authenticated; please login and try again.`;
75055
- cms.alerts.error(authMessage);
75056
- console.error(authMessage);
75640
+ dispatchSessionExpired(cms.events);
75057
75641
  return false;
75058
75642
  }
75059
75643
  };
@@ -75236,7 +75820,7 @@ const RenderForm$1 = ({
75236
75820
  onSubmit: async (values) => {
75237
75821
  try {
75238
75822
  const folderName2 = folder.fullyQualifiedName ? folder.name : "";
75239
- await createDocument(
75823
+ const result = await createDocument(
75240
75824
  cms,
75241
75825
  collection,
75242
75826
  template,
@@ -75244,6 +75828,7 @@ const RenderForm$1 = ({
75244
75828
  folderName2,
75245
75829
  values
75246
75830
  );
75831
+ if (result === false) return;
75247
75832
  cms.alerts.success("Document created!");
75248
75833
  setTimeout(() => {
75249
75834
  navigate(
@@ -75251,6 +75836,7 @@ const RenderForm$1 = ({
75251
75836
  );
75252
75837
  }, 10);
75253
75838
  } catch (error) {
75839
+ if (isSessionExpiredError(error)) throw error;
75254
75840
  const defaultErrorText = "There was a problem saving your document.";
75255
75841
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
75256
75842
  cms.alerts.error(
@@ -75323,17 +75909,19 @@ const useGetDocument = (cms, collectionName, relativePath) => {
75323
75909
  setDocument(response.document);
75324
75910
  }
75325
75911
  } else if (!isCancelled) {
75326
- cms.events.dispatch({ type: "cms:session-expired" });
75912
+ dispatchSessionExpired(cms.events);
75327
75913
  setDocument(void 0);
75328
75914
  }
75329
75915
  } catch (error2) {
75330
75916
  if (!isCancelled) {
75331
- cms.alerts.error(
75332
- `[${error2.name}] GetDocument failed: ${error2.message}`
75333
- );
75334
- console.error(error2);
75335
75917
  setDocument(void 0);
75336
- setError(error2);
75918
+ if (!isSessionExpiredError(error2)) {
75919
+ cms.alerts.error(
75920
+ `[${error2.name}] GetDocument failed: ${error2.message}`
75921
+ );
75922
+ console.error(error2);
75923
+ setError(error2);
75924
+ }
75337
75925
  }
75338
75926
  }
75339
75927
  if (!isCancelled) {
@@ -75419,9 +76007,7 @@ const updateDocument = async (cms, relativePath, collection, mutationInfo, value
75419
76007
  if (await api.isAuthenticated()) {
75420
76008
  await api.updateDocument(collection, relativePath, params);
75421
76009
  } else {
75422
- const authMessage = `UpdateDocument failed: User is no longer authenticated; please login and try again.`;
75423
- cms.alerts.error(authMessage);
75424
- console.error(authMessage);
76010
+ dispatchSessionExpired(cms.events);
75425
76011
  return false;
75426
76012
  }
75427
76013
  };
@@ -75499,15 +76085,17 @@ const RenderForm = ({
75499
76085
  initialValues: document2._values,
75500
76086
  onSubmit: async (values) => {
75501
76087
  try {
75502
- await updateDocument(
76088
+ const result = await updateDocument(
75503
76089
  cms,
75504
76090
  relativePath,
75505
76091
  collection,
75506
76092
  mutationInfo,
75507
76093
  values
75508
76094
  );
76095
+ if (result === false) return;
75509
76096
  cms.alerts.success("Document updated!");
75510
76097
  } catch (error) {
76098
+ if (isSessionExpiredError(error)) throw error;
75511
76099
  cms.alerts.error(
75512
76100
  () => ErrorDialog({
75513
76101
  title: "There was a problem saving your document",
@@ -75717,6 +76305,9 @@ const CheckSchema = ({
75717
76305
  );
75718
76306
  }
75719
76307
  }).catch((error) => {
76308
+ if (isSessionExpiredError(error)) {
76309
+ return;
76310
+ }
75720
76311
  if (error.message.includes(ERR_NOT_INDEXED)) {
75721
76312
  setSchemaMissingError(true);
75722
76313
  } else {
@@ -76181,6 +76772,7 @@ export {
76181
76772
  Select$1 as Select,
76182
76773
  SelectField,
76183
76774
  SelectFieldPlugin,
76775
+ SessionExpiredError,
76184
76776
  SettingsIcon,
76185
76777
  StrikethroughIcon,
76186
76778
  StyledFile,
@@ -76233,6 +76825,7 @@ export {
76233
76825
  get,
76234
76826
  getFilteredBranchList,
76235
76827
  getStaticPropsForTina,
76828
+ isSessionExpiredError,
76236
76829
  passwordFieldClasses,
76237
76830
  resolveField,
76238
76831
  safeAssertShape,