tinacms 3.12.0 → 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.
Files changed (33) hide show
  1. package/dist/admin/components/GetDocument.d.ts +1 -1
  2. package/dist/admin/components/UnableToLoadModal.d.ts +4 -0
  3. package/dist/index.js +1798 -1045
  4. package/dist/internalClient/authProvider.d.ts +8 -0
  5. package/dist/internalClient/index.d.ts +14 -2
  6. package/dist/internalClient/types.d.ts +11 -0
  7. package/dist/rich-text/index.js +35 -35
  8. package/dist/rich-text/static.js +40 -37
  9. package/dist/toolkit/alerts/alerts.d.ts +7 -1
  10. package/dist/toolkit/components/media/media-manager.d.ts +3 -1
  11. package/dist/toolkit/components/media/utils.d.ts +12 -0
  12. package/dist/toolkit/core/cms.d.ts +2 -2
  13. package/dist/toolkit/core/errors.d.ts +1 -0
  14. package/dist/toolkit/core/index.d.ts +2 -0
  15. package/dist/toolkit/core/media-store.default.d.ts +41 -15
  16. package/dist/toolkit/core/media.d.ts +18 -0
  17. package/dist/toolkit/core/session-expired.d.ts +6 -0
  18. package/dist/toolkit/fields/components/image-upload/image-upload.d.ts +2 -0
  19. package/dist/toolkit/fields/plugins/blocks-field-plugin/block-selector-big.d.ts +2 -1
  20. package/dist/toolkit/fields/plugins/blocks-field-plugin/block-selector.d.ts +2 -1
  21. package/dist/toolkit/fields/plugins/field-props.d.ts +12 -1
  22. package/dist/toolkit/fields/plugins/group-list-field-plugin.d.ts +2 -1
  23. package/dist/toolkit/fields/plugins/image-field-plugin.d.ts +2 -0
  24. package/dist/toolkit/fields/plugins/mdx-field-plugin/index.d.ts +5 -1
  25. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/plugins/create-html-block/index.d.ts +2 -2
  26. package/dist/toolkit/form-builder/create-branch-modal.d.ts +2 -1
  27. package/dist/toolkit/form-builder/editorial-workflow-utils.d.ts +2 -0
  28. package/dist/toolkit/index.d.ts +1 -1
  29. package/dist/toolkit/plugin-branch-switcher/branch-switcher-legacy.d.ts +1 -1
  30. package/dist/toolkit/plugin-branch-switcher/branch-switcher.d.ts +6 -2
  31. package/dist/utils/branch-name.d.ts +3 -0
  32. package/package.json +9 -10
  33. package/dist/toolkit/plugin-branch-switcher/format-branch-name.d.ts +0 -8
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";
@@ -52,8 +51,9 @@ import PropTypes from "prop-types";
52
51
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from "@dnd-kit/core";
53
52
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, defaultAnimateLayoutChanges } from "@dnd-kit/sortable";
54
53
  import { CSS } from "@dnd-kit/utilities";
55
- import { format as format$3, formatDistanceToNow } from "date-fns";
56
- import { useReactTable, getSortedRowModel, getCoreRowModel, flexRender, getExpandedRowModel, getFilteredRowModel } from "@tanstack/react-table";
54
+ import { buildSchema, print, getIntrospectionQuery, buildClientSchema, parse as parse$3 } from "graphql";
55
+ import { diff as diff$1 } from "@graphql-inspector/core";
56
+ import { processDocumentForIndexing, queryToSearchIndexQuery, optionsToSearchIndexOptions, parseSearchIndexResponse } from "@tinacms/search/index-client";
57
57
  import { HexColorPicker } from "react-colorful";
58
58
  import { Command as Command$3 } from "@udecode/cmdk";
59
59
  import { isHotkey } from "is-hotkey";
@@ -62,19 +62,18 @@ import { ExitBreakPlugin, SoftBreakPlugin } from "@udecode/plate-break/react";
62
62
  import { ResetNodePlugin } from "@udecode/plate-reset-node/react";
63
63
  import { TrailingBlockPlugin } from "@udecode/plate-trailing-block";
64
64
  import { BaseBlockquotePlugin } from "@udecode/plate-block-quote";
65
+ import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
65
66
  import { HeadingPlugin } from "@udecode/plate-heading/react";
66
67
  import { IndentListPlugin } from "@udecode/plate-indent-list/react";
67
68
  import { NodeIdPlugin } from "@udecode/plate-node-id";
68
69
  import { offset, flip, shift, limitShift, useVirtualFloating, getSelectionBoundingClientRect } from "@udecode/plate-floating";
69
70
  import { getLinkAttributes } from "@udecode/plate-link";
70
- import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
71
71
  import * as yup from "yup";
72
+ import { format as format$3, formatDistanceToNow } from "date-fns";
72
73
  import { enUS } from "date-fns/locale";
73
74
  import { DayPicker } from "react-day-picker";
74
75
  import * as SelectPrimitive from "@radix-ui/react-select";
75
- import { buildSchema, print, getIntrospectionQuery, buildClientSchema, parse as parse$3 } from "graphql";
76
- import { diff as diff$1 } from "@graphql-inspector/core";
77
- import { processDocumentForIndexing, queryToSearchIndexQuery, optionsToSearchIndexOptions, parseSearchIndexResponse } from "@tinacms/search/index-client";
76
+ import { useReactTable, getSortedRowModel, getCoreRowModel, flexRender, getExpandedRowModel, getFilteredRowModel } from "@tanstack/react-table";
78
77
  import * as dropzone from "react-dropzone";
79
78
  import gql from "graphql-tag";
80
79
  import { useLocation, NavLink, useNavigate, useParams, Navigate, Link as Link$1, HashRouter, Routes, Route } from "react-router-dom";
@@ -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"
@@ -4225,6 +4271,8 @@ const Button$2 = ({
4225
4271
  medium: `text-sm h-10 px-8`,
4226
4272
  custom: ``
4227
4273
  };
4274
+ const isDisabled = disabled || busy;
4275
+ const inertProps = typeof Tag2 !== "string" || Tag2 === "button" ? { disabled: isDisabled } : isDisabled && { "aria-disabled": true, tabIndex: -1, href: void 0 };
4228
4276
  return /* @__PURE__ */ React.createElement(
4229
4277
  Tag2,
4230
4278
  {
@@ -4236,7 +4284,9 @@ const Button$2 = ({
4236
4284
  roundedClasses[rounded],
4237
4285
  className
4238
4286
  ),
4239
- ...props
4287
+ ...props,
4288
+ ...inertProps,
4289
+ onClick: isDisabled ? void 0 : props.onClick
4240
4290
  },
4241
4291
  busy ? /* @__PURE__ */ React.createElement(LoadingDots, { color: "currentColor" }) : children
4242
4292
  );
@@ -4279,7 +4329,8 @@ const IconButton = ({
4279
4329
  stateClasses[state],
4280
4330
  className
4281
4331
  ),
4282
- ...props
4332
+ ...props,
4333
+ disabled: disabled || busy
4283
4334
  },
4284
4335
  children
4285
4336
  );
@@ -4469,6 +4520,54 @@ function DropdownMenuItem({
4469
4520
  }
4470
4521
  );
4471
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
+ }
4472
4571
  function DropdownMenuSeparator({
4473
4572
  className,
4474
4573
  ...props
@@ -4698,6 +4797,56 @@ const SortableProvider = ({
4698
4797
  }) => {
4699
4798
  return /* @__PURE__ */ React__default.createElement(SortableContext, { items, strategy: verticalListSortingStrategy }, children);
4700
4799
  };
4800
+ const formatDefaultBranchName = (filePath, crudType) => {
4801
+ let result = filePath;
4802
+ const contentPrefix = "content/";
4803
+ if (result.startsWith(contentPrefix)) {
4804
+ result = result.substring(contentPrefix.length);
4805
+ }
4806
+ const lastDot = result.lastIndexOf(".");
4807
+ const lastSlash = Math.max(result.lastIndexOf("/"), result.lastIndexOf("\\"));
4808
+ if (lastDot > lastSlash && lastDot > 0) {
4809
+ result = result.slice(0, lastDot);
4810
+ }
4811
+ result = normalizeBranchName(result);
4812
+ if (crudType === "delete") {
4813
+ result = `❌-${result}`;
4814
+ }
4815
+ return result;
4816
+ };
4817
+ const trimRefComponent = (part) => {
4818
+ let result = part.replace(/\.{2,}/g, ".");
4819
+ while (result.startsWith(".")) {
4820
+ result = result.slice(1);
4821
+ }
4822
+ let previous = "";
4823
+ while (previous !== result) {
4824
+ previous = result;
4825
+ if (result.endsWith(".lock")) {
4826
+ result = result.slice(0, -".lock".length);
4827
+ } else if (result.endsWith(".")) {
4828
+ result = result.slice(0, -1);
4829
+ }
4830
+ }
4831
+ return result;
4832
+ };
4833
+ const normalizeBranchName = (name2) => name2.replace(/[\x00-\x20\x7f~^:?*\[\\]+/g, "-").replace(/@\{/g, "-").split("/").map(trimRefComponent).filter(Boolean).join("/");
4834
+ function formatBranchName(str) {
4835
+ let result = "";
4836
+ let replacingInvalidChars = false;
4837
+ for (const char of str.toLowerCase()) {
4838
+ const code = char.charCodeAt(0);
4839
+ const isValid = char === "/" || char === "-" || char === "_" || code >= 48 && code <= 57 || code >= 97 && code <= 122;
4840
+ if (isValid) {
4841
+ result += char;
4842
+ replacingInvalidChars = false;
4843
+ } else if (!replacingInvalidChars) {
4844
+ result += "-";
4845
+ replacingInvalidChars = true;
4846
+ }
4847
+ }
4848
+ return result;
4849
+ }
4701
4850
  const textFieldClasses = "shadow-inner focus:shadow-outline focus:border-blue-500 focus:outline-none block text-base placeholder:text-gray-300 px-3 py-2 text-gray-600 w-full bg-white border border-gray-200 transition-all ease-out duration-150 focus:text-gray-900 rounded";
4702
4851
  const disabledClasses$1 = "opacity-50 pointer-events-none cursor-not-allowed";
4703
4852
  const BaseTextField = React.forwardRef(({ className, disabled, ...rest }, ref) => {
@@ -5502,12 +5651,38 @@ const dropzoneAcceptFromString = (str) => {
5502
5651
  ...(str || DEFAULT_MEDIA_UPLOAD_TYPES).split(",").map((x) => ({ [x]: [] }))
5503
5652
  );
5504
5653
  };
5505
- const isImage = (filename) => {
5506
- return /\.(gif|jpg|jpeg|tiff|png|svg|webp|avif)(\?.*)?$/i.test(filename);
5507
- };
5508
- const isVideo = (filename) => {
5509
- 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;
5510
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 ?? ""));
5511
5686
  const absoluteImgURL = (str) => {
5512
5687
  if (str.startsWith("http")) return str;
5513
5688
  return `${window.location.origin}${str}`;
@@ -5569,12 +5744,13 @@ const StyledImage = ({ src }) => {
5569
5744
  const StyledFile = ({ src }) => {
5570
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));
5571
5746
  };
5572
- const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading }, ref) => {
5747
+ const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading, accept }, ref) => {
5573
5748
  const cms = useCMS();
5574
5749
  const { getRootProps, getInputProps } = useDropzone$1({
5575
- accept: dropzoneAcceptFromString(
5576
- cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
5577
- ),
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),
5578
5754
  onDrop,
5579
5755
  noClick: !!onClick
5580
5756
  });
@@ -6193,6 +6369,7 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6193
6369
  return /* @__PURE__ */ React__default.createElement(
6194
6370
  ListFieldMeta,
6195
6371
  {
6372
+ "data-test": `list-${field.name}`,
6196
6373
  name: input.name,
6197
6374
  label: field.label,
6198
6375
  description: field.description,
@@ -6203,6 +6380,7 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6203
6380
  actions: (!fixedLength || fixedLength && !isMax) && /* @__PURE__ */ React__default.createElement(
6204
6381
  IconButton,
6205
6382
  {
6383
+ "data-test": `add-item-${field.name}`,
6206
6384
  onClick: addItem,
6207
6385
  disabled: isMax,
6208
6386
  variant: "primary",
@@ -6295,7 +6473,14 @@ const Item$1 = ({
6295
6473
  /* @__PURE__ */ React__default.createElement(GroupLabel, null, title),
6296
6474
  /* @__PURE__ */ React__default.createElement(Pencil, { className: "h-5 w-auto text-gray-200 group-hover:text-inherit transition-colors duration-150 ease-out" })
6297
6475
  ),
6298
- (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React__default.createElement(ItemDeleteButton, { disabled: isMin, onClick: removeItem })
6476
+ (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React__default.createElement(
6477
+ ItemDeleteButton,
6478
+ {
6479
+ disabled: isMin,
6480
+ onClick: removeItem,
6481
+ fieldName: `${field.name}.${index}`
6482
+ }
6483
+ )
6299
6484
  )));
6300
6485
  };
6301
6486
  const ItemClickTarget = ({ children, ...props }) => {
@@ -6342,11 +6527,16 @@ const ItemHeader = ({
6342
6527
  children
6343
6528
  );
6344
6529
  };
6345
- const ItemDeleteButton = ({ onClick, disabled = false }) => {
6530
+ const ItemDeleteButton = ({
6531
+ onClick,
6532
+ disabled = false,
6533
+ fieldName = ""
6534
+ }) => {
6346
6535
  return /* @__PURE__ */ React__default.createElement(
6347
6536
  "button",
6348
6537
  {
6349
6538
  type: "button",
6539
+ "data-test": fieldName ? `delete-item-${fieldName}` : "delete-item",
6350
6540
  className: `w-8 px-1 py-2.5 flex items-center justify-center text-gray-200 hover:opacity-100 opacity-30 hover:bg-gray-50 ${disabled && "pointer-events-none opacity-30 cursor-not-allowed"}`,
6351
6541
  onClick
6352
6542
  },
@@ -6389,7 +6579,8 @@ const PopoverContent = React.forwardRef(({ className, align = "center", sideOffs
6389
6579
  PopoverContent.displayName = PopoverPrimitive.Content.displayName;
6390
6580
  const BlockSelector = ({
6391
6581
  templates,
6392
- addItem
6582
+ addItem,
6583
+ fieldName
6393
6584
  }) => {
6394
6585
  const showFilter = React.useMemo(() => {
6395
6586
  return Object.entries(templates).length > 6;
@@ -6404,6 +6595,7 @@ const BlockSelector = ({
6404
6595
  return /* @__PURE__ */ React.createElement(Popover, { open, onOpenChange: setOpen }, /* @__PURE__ */ React.createElement(PopoverTrigger, { asChild: true }, /* @__PURE__ */ React.createElement("span", null, /* @__PURE__ */ React.createElement(
6405
6596
  IconButton,
6406
6597
  {
6598
+ "data-test": fieldName ? `add-item-${fieldName}` : void 0,
6407
6599
  variant: open ? "secondary" : "primary",
6408
6600
  size: "small",
6409
6601
  className: `${open ? `rotate-45 pointer-events-none` : ``}`
@@ -6703,7 +6895,8 @@ const constructUrlWithBasePath = (url, basePath) => {
6703
6895
  const BlockSelectorBig = ({
6704
6896
  templates,
6705
6897
  addItem,
6706
- label
6898
+ label,
6899
+ fieldName
6707
6900
  }) => {
6708
6901
  const FormPortal = useFormPortal();
6709
6902
  const [pickerIsOpen, setPickerIsOpen] = React.useState(false);
@@ -6748,6 +6941,7 @@ const BlockSelectorBig = ({
6748
6941
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
6749
6942
  IconButton,
6750
6943
  {
6944
+ "data-test": fieldName ? `add-item-${fieldName}` : void 0,
6751
6945
  variant: pickerIsOpen ? "secondary" : "primary",
6752
6946
  size: "small",
6753
6947
  className: `${pickerIsOpen ? "rotate-45 pointer-events-none" : ""}`,
@@ -6962,6 +7156,7 @@ const Blocks = ({
6962
7156
  return /* @__PURE__ */ React.createElement(
6963
7157
  ListFieldMeta,
6964
7158
  {
7159
+ "data-test": `list-${field.name}`,
6965
7160
  name: input.name,
6966
7161
  label: field.label,
6967
7162
  description: field.description,
@@ -6970,9 +7165,17 @@ const Blocks = ({
6970
7165
  index,
6971
7166
  tinaForm,
6972
7167
  actions: (!fixedLength || fixedLength && !isMax) && // @ts-ignore
6973
- (!field.visualSelector ? /* @__PURE__ */ React.createElement(BlockSelector, { templates: field.templates, addItem }) : /* @__PURE__ */ React.createElement(
7168
+ (!field.visualSelector ? /* @__PURE__ */ React.createElement(
7169
+ BlockSelector,
7170
+ {
7171
+ templates: field.templates,
7172
+ addItem,
7173
+ fieldName: field.name
7174
+ }
7175
+ ) : /* @__PURE__ */ React.createElement(
6974
7176
  BlockSelectorBig,
6975
7177
  {
7178
+ fieldName: field.name,
6976
7179
  label: field.label || field.name,
6977
7180
  templates: field.templates,
6978
7181
  addItem
@@ -7074,7 +7277,14 @@ const BlockListItem = ({
7074
7277
  /* @__PURE__ */ React.createElement(GroupLabel, null, label || template.label),
7075
7278
  isNew && /* @__PURE__ */ React.createElement("span", { className: "mr-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] border-[0.5px] border-tina-orange/50 font-semibold bg-tina-orange/10 text-tina-orange leading-none" }, "NEW"),
7076
7279
  /* @__PURE__ */ React.createElement(Pencil, { className: "h-5 w-auto text-gray-200 group-hover:text-inherit transition-colors duration-150 ease-out" })
7077
- ), (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React.createElement(ItemDeleteButton, { disabled: isMin, onClick: removeItem }))));
7280
+ ), (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React.createElement(
7281
+ ItemDeleteButton,
7282
+ {
7283
+ disabled: isMin,
7284
+ onClick: removeItem,
7285
+ fieldName: `${field.name}.${index}`
7286
+ }
7287
+ ))));
7078
7288
  };
7079
7289
  const InvalidBlockListItem = ({
7080
7290
  tinaForm,
@@ -7090,7 +7300,13 @@ const InvalidBlockListItem = ({
7090
7300
  isDragging: snapshot.isDragging,
7091
7301
  dragHandleProps: provider.dragHandleProps
7092
7302
  }
7093
- ), /* @__PURE__ */ React.createElement(ItemClickTarget, null, /* @__PURE__ */ React.createElement(GroupLabel, { error: true }, "Invalid Block")), /* @__PURE__ */ React.createElement(ItemDeleteButton, { onClick: removeItem })));
7303
+ ), /* @__PURE__ */ React.createElement(ItemClickTarget, null, /* @__PURE__ */ React.createElement(GroupLabel, { error: true }, "Invalid Block")), /* @__PURE__ */ React.createElement(
7304
+ ItemDeleteButton,
7305
+ {
7306
+ onClick: removeItem,
7307
+ fieldName: `${field.name}.${index}`
7308
+ }
7309
+ )));
7094
7310
  };
7095
7311
  const BlocksField = Blocks;
7096
7312
  const BlocksFieldPlugin = {
@@ -7148,6 +7364,7 @@ const List = ({ tinaForm, form, field, input, meta, index }) => {
7148
7364
  return /* @__PURE__ */ React.createElement(
7149
7365
  ListFieldMeta,
7150
7366
  {
7367
+ "data-test": `list-${field.name}`,
7151
7368
  name: input.name,
7152
7369
  label: field.label,
7153
7370
  description: field.description,
@@ -7157,6 +7374,7 @@ const List = ({ tinaForm, form, field, input, meta, index }) => {
7157
7374
  actions: /* @__PURE__ */ React.createElement(
7158
7375
  IconButton,
7159
7376
  {
7377
+ "data-test": `add-item-${field.name}`,
7160
7378
  onClick: addItem,
7161
7379
  variant: "primary",
7162
7380
  size: "small",
@@ -7215,7 +7433,14 @@ const Item = ({
7215
7433
  isDragging: snapshot.isDragging,
7216
7434
  dragHandleProps: provider.dragHandleProps
7217
7435
  }
7218
- ), /* @__PURE__ */ React.createElement(ItemClickTarget, null, /* @__PURE__ */ React.createElement(FieldsBuilder, { padding: false, form: tinaForm, fields })), (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React.createElement(ItemDeleteButton, { disabled: isMin, onClick: removeItem })));
7436
+ ), /* @__PURE__ */ React.createElement(ItemClickTarget, null, /* @__PURE__ */ React.createElement(FieldsBuilder, { padding: false, form: tinaForm, fields })), (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React.createElement(
7437
+ ItemDeleteButton,
7438
+ {
7439
+ disabled: isMin,
7440
+ onClick: removeItem,
7441
+ fieldName: `${field.name}.${index}`
7442
+ }
7443
+ )));
7219
7444
  };
7220
7445
  const ListField = List;
7221
7446
  const ListFieldPlugin = {
@@ -7245,6 +7470,16 @@ const ImageField = wrapFieldsWithMeta(
7245
7470
  var _a, _b;
7246
7471
  if (media) {
7247
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
+ }
7248
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;
7249
7484
  props.input.onChange(parsedValue);
7250
7485
  }
@@ -7257,12 +7492,14 @@ const ImageField = wrapFieldsWithMeta(
7257
7492
  value,
7258
7493
  src,
7259
7494
  loading: isImgUploading,
7495
+ accept: props.field.accept,
7260
7496
  onClick: () => {
7261
7497
  const directory = uploadDir(props.form.getState().values);
7262
7498
  cms.media.open({
7263
7499
  allowDelete: true,
7264
7500
  directory,
7265
- onSelect: onChange
7501
+ onSelect: onChange,
7502
+ accept: props.field.accept
7266
7503
  });
7267
7504
  },
7268
7505
  onDrop: async ([file], fileRejections) => {
@@ -8647,6 +8884,34 @@ const InfoBox = ({
8647
8884
  InfoBoxComponent.displayName = "InfoBox";
8648
8885
  return InfoBoxComponent;
8649
8886
  };
8887
+ function useLocalStorage(key, initialValue) {
8888
+ const [storedValue, setStoredValue] = React.useState(() => {
8889
+ try {
8890
+ const valueFromStorage = typeof window !== "undefined" && window.localStorage && window.localStorage.getItem(key);
8891
+ if (valueFromStorage != null) {
8892
+ return JSON.parse(valueFromStorage);
8893
+ }
8894
+ } catch (error) {
8895
+ }
8896
+ return initialValue;
8897
+ });
8898
+ React.useEffect(() => {
8899
+ const valueFromStorage = window.localStorage && window.localStorage.getItem(key);
8900
+ if (valueFromStorage != null && valueFromStorage != void 0) {
8901
+ setStoredValue(JSON.parse(valueFromStorage));
8902
+ }
8903
+ }, [key]);
8904
+ const setValue = (value) => {
8905
+ try {
8906
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
8907
+ setStoredValue(valueToStore);
8908
+ localStorage.setItem(key, JSON.stringify(valueToStore));
8909
+ } catch (error) {
8910
+ console.log(error);
8911
+ }
8912
+ };
8913
+ return [storedValue, setValue];
8914
+ }
8650
8915
  const Badge = ({
8651
8916
  children,
8652
8917
  calloutStyle = "warning",
@@ -8708,11 +8973,6 @@ const useBranchData = () => {
8708
8973
  }, [branchData.currentBranch]);
8709
8974
  return branchData;
8710
8975
  };
8711
- function formatBranchName$1(str) {
8712
- const pattern = /[^/\w-]+/g;
8713
- const formattedStr = str.replace(pattern, "");
8714
- return formattedStr.toLowerCase();
8715
- }
8716
8976
  const BranchSwitcherLegacy = ({
8717
8977
  listBranches,
8718
8978
  createBranch,
@@ -8735,7 +8995,7 @@ const BranchSwitcherLegacy = ({
8735
8995
  const handleCreateBranch = React.useCallback((value) => {
8736
8996
  setListState("loading");
8737
8997
  createBranch({
8738
- branchName: formatBranchName$1(value),
8998
+ branchName: normalizeBranchName(formatBranchName(value)),
8739
8999
  baseBranch: currentBranch
8740
9000
  }).then(async (createdBranchName) => {
8741
9001
  cms.alerts.success("Branch created.");
@@ -8905,6 +9165,9 @@ const BranchSelector = ({
8905
9165
  ));
8906
9166
  };
8907
9167
  const CreateBranch = ({ currentBranch, newBranchName, onCreateBranch, setNewBranchName }) => {
9168
+ const normalizedBranchName = normalizeBranchName(
9169
+ formatBranchName(newBranchName)
9170
+ );
8908
9171
  return /* @__PURE__ */ React.createElement("div", { className: "border-t border-gray-150 pt-4 mt-3 flex flex-col gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm" }, "Create a new branch from ", /* @__PURE__ */ React.createElement("b", null, currentBranch), ". Once created you will need to wait for indexing to complete before you can switch branches."), /* @__PURE__ */ React.createElement("div", { className: "flex justify-between items-center w-full gap-3" }, /* @__PURE__ */ React.createElement(
8909
9172
  BaseTextField,
8910
9173
  {
@@ -8918,8 +9181,11 @@ const CreateBranch = ({ currentBranch, newBranchName, onCreateBranch, setNewBran
8918
9181
  className: "flex-0 flex items-center gap-2 whitespace-nowrap",
8919
9182
  size: "medium",
8920
9183
  variant: "white",
8921
- disabled: newBranchName === "",
8922
- onClick: () => onCreateBranch(newBranchName)
9184
+ disabled: normalizedBranchName === "",
9185
+ onClick: () => {
9186
+ if (!normalizedBranchName) return;
9187
+ onCreateBranch(normalizedBranchName);
9188
+ }
8923
9189
  },
8924
9190
  /* @__PURE__ */ React.createElement(Plus, { className: "w-5 h-auto opacity-70" }),
8925
9191
  " Create Branch"
@@ -9373,22 +9639,6 @@ const PullRequestCell = ({
9373
9639
  }
9374
9640
  return null;
9375
9641
  };
9376
- function formatBranchName(str) {
9377
- let result = "";
9378
- let replacingInvalidChars = false;
9379
- for (const char of str.toLowerCase()) {
9380
- const code = char.charCodeAt(0);
9381
- const isValid = char === "/" || char === "-" || char === "_" || code >= 48 && code <= 57 || code >= 97 && code <= 122;
9382
- if (isValid) {
9383
- result += char;
9384
- replacingInvalidChars = false;
9385
- } else if (!replacingInvalidChars) {
9386
- result += "-";
9387
- replacingInvalidChars = true;
9388
- }
9389
- }
9390
- return result;
9391
- }
9392
9642
  const tableHeadingStyle = "px-3 py-3 text-left text-xs font-bold text-gray-700 tracking-wider sticky top-0 bg-gray-100 z-20 border-b-2 border-gray-200 ";
9393
9643
  const BranchSwitcher = (props) => {
9394
9644
  const cms = useCMS$1();
@@ -9429,7 +9679,7 @@ const EditoralBranchSwitcher = ({
9429
9679
  const handleCreateBranch = React.useCallback((value) => {
9430
9680
  setListState("loading");
9431
9681
  createBranch({
9432
- branchName: formatBranchName(value),
9682
+ branchName: normalizeBranchName(formatBranchName(value)),
9433
9683
  baseBranch: currentBranch
9434
9684
  }).then(async (createdBranchName) => {
9435
9685
  cms.alerts.success("Branch created.");
@@ -9573,46 +9823,58 @@ const sortBranchListFn = (sortValue) => {
9573
9823
  }
9574
9824
  };
9575
9825
  };
9576
- const BranchCreator = ({ setViewState, handleCreateBranch, currentBranch }) => {
9826
+ const BranchCreator = ({
9827
+ setViewState,
9828
+ handleCreateBranch,
9829
+ currentBranch
9830
+ }) => {
9577
9831
  const [branchName, setBranchName] = React.useState("");
9578
- return /* @__PURE__ */ React.createElement("form", null, /* @__PURE__ */ React.createElement("div", { className: "" }, /* @__PURE__ */ React.createElement("p", { className: "text-base text-gray-700 mb-4" }, "Create a new branch from ", /* @__PURE__ */ React.createElement("strong", null, currentBranch), "."), /* @__PURE__ */ React.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React.createElement(FieldLabel, { name: "current-branch-name" }, "Current Branch Name"), /* @__PURE__ */ React.createElement(
9579
- BaseTextField,
9580
- {
9581
- name: "current-branch-name",
9582
- value: currentBranch,
9583
- disabled: true
9584
- }
9585
- )), /* @__PURE__ */ React.createElement("div", { className: "mb-6" }, /* @__PURE__ */ React.createElement(FieldLabel, { name: "branch-name" }, "New Branch Name"), /* @__PURE__ */ React.createElement(
9586
- PrefixedTextField,
9587
- {
9588
- placeholder: "",
9589
- name: "branch-name",
9590
- value: branchName,
9591
- onChange: (e) => setBranchName(e.target.value)
9592
- }
9593
- ))), /* @__PURE__ */ React.createElement("div", { className: "w-full flex justify-between gap-4 items-center" }, /* @__PURE__ */ React.createElement(
9594
- Button$2,
9595
- {
9596
- style: { flexGrow: 1 },
9597
- onClick: () => {
9598
- setViewState("list");
9832
+ const normalizedBranchName = normalizeBranchName(
9833
+ formatBranchName(branchName)
9834
+ );
9835
+ return (
9836
+ // Submit is handled by the button below; without this the implicit
9837
+ // submission from pressing Enter in the field reloads the page
9838
+ /* @__PURE__ */ React.createElement("form", { onSubmit: (e) => e.preventDefault() }, /* @__PURE__ */ React.createElement("div", { className: "" }, /* @__PURE__ */ React.createElement("p", { className: "text-base text-gray-700 mb-4" }, "Create a new branch from ", /* @__PURE__ */ React.createElement("strong", null, currentBranch), "."), /* @__PURE__ */ React.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React.createElement(FieldLabel, { name: "current-branch-name" }, "Current Branch Name"), /* @__PURE__ */ React.createElement(
9839
+ BaseTextField,
9840
+ {
9841
+ name: "current-branch-name",
9842
+ value: currentBranch,
9843
+ disabled: true
9599
9844
  }
9600
- },
9601
- "Cancel"
9602
- ), /* @__PURE__ */ React.createElement(
9603
- Button$2,
9604
- {
9605
- variant: "primary",
9606
- type: "submit",
9607
- style: { flexGrow: 2 },
9608
- disabled: branchName === "",
9609
- onClick: () => {
9610
- handleCreateBranch("tina/" + branchName);
9845
+ )), /* @__PURE__ */ React.createElement("div", { className: "mb-6" }, /* @__PURE__ */ React.createElement(FieldLabel, { name: "branch-name" }, "New Branch Name"), /* @__PURE__ */ React.createElement(
9846
+ PrefixedTextField,
9847
+ {
9848
+ placeholder: "",
9849
+ name: "branch-name",
9850
+ value: branchName,
9851
+ onChange: (e) => setBranchName(e.target.value)
9611
9852
  }
9612
- },
9613
- "Create Branch ",
9614
- /* @__PURE__ */ React.createElement(GitBranch, { className: "w-5 h-full ml-1 opacity-70" })
9615
- )));
9853
+ ))), /* @__PURE__ */ React.createElement("div", { className: "w-full flex justify-between gap-4 items-center" }, /* @__PURE__ */ React.createElement(
9854
+ Button$2,
9855
+ {
9856
+ style: { flexGrow: 1 },
9857
+ onClick: () => {
9858
+ setViewState("list");
9859
+ }
9860
+ },
9861
+ "Cancel"
9862
+ ), /* @__PURE__ */ React.createElement(
9863
+ Button$2,
9864
+ {
9865
+ variant: "primary",
9866
+ type: "submit",
9867
+ style: { flexGrow: 2 },
9868
+ disabled: normalizedBranchName === "",
9869
+ onClick: () => {
9870
+ if (!normalizedBranchName) return;
9871
+ handleCreateBranch("tina/" + normalizedBranchName);
9872
+ }
9873
+ },
9874
+ "Create Branch ",
9875
+ /* @__PURE__ */ React.createElement(GitBranch, { className: "w-5 h-full ml-1 opacity-70" })
9876
+ )))
9877
+ );
9616
9878
  };
9617
9879
  const extractPullRequestId = (url) => {
9618
9880
  return url.split("/").pop() || "";
@@ -9809,9 +10071,21 @@ let Alerts$1 = class Alerts {
9809
10071
  this.add(level, message, timeout);
9810
10072
  }
9811
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);
9812
10080
  this.events = events;
9813
10081
  this.map = map;
9814
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
+ });
9815
10089
  }
9816
10090
  setMap(eventsToAlerts) {
9817
10091
  this.map = {
@@ -9820,6 +10094,10 @@ let Alerts$1 = class Alerts {
9820
10094
  };
9821
10095
  }
9822
10096
  add(level, message, timeout = 4e3) {
10097
+ if (this.suppressed) {
10098
+ return () => {
10099
+ };
10100
+ }
9823
10101
  const alert = {
9824
10102
  level,
9825
10103
  message,
@@ -9860,219 +10138,6 @@ let Alerts$1 = class Alerts {
9860
10138
  return this.add("error", message, timeout);
9861
10139
  }
9862
10140
  };
9863
- class PluginTypeManager {
9864
- constructor(events) {
9865
- /**
9866
- * @ignore
9867
- */
9868
- __publicField(this, "plugins", {});
9869
- this.events = events;
9870
- }
9871
- /**
9872
- * Gets the [[PluginType|collection of plugins]] for the given type.
9873
- *
9874
- * #### Example: Basic Usage
9875
- *
9876
- * ```ts
9877
- * const colorPlugins = cms.plugins.get("color")
9878
- * ```
9879
- *
9880
- * #### Example: Advanced Types
9881
- *
9882
- * A type param can be added to specify the kind of Plugin
9883
- * that is being listed.
9884
- *
9885
- * ```ts
9886
- * const colorPlugins = cms.plugins.get<ColorPlugin>("color")
9887
- * ```
9888
- *
9889
- * @param type The type of plugins to be retrieved
9890
- * @typeparam P A subclass of Plugin. Optional.
9891
- */
9892
- getType(type2) {
9893
- return this.plugins[type2] = this.plugins[type2] || new PluginType(type2, this.events);
9894
- }
9895
- /**
9896
- * An alias to [[get]]
9897
- *
9898
- * ### !DEPRECATED!
9899
- *
9900
- * This name is unnecessarily verbose and weird.
9901
- */
9902
- findOrCreateMap(type2) {
9903
- return this.getType(type2);
9904
- }
9905
- /**
9906
- * Adds a Plugin to the CMS.
9907
- *
9908
- * #### Example: Basic Usage
9909
- *
9910
- * ```js
9911
- * cms.plugins.add({ __type: "color", name: "red" })
9912
- * ```
9913
- *
9914
- * #### Example: Advanced Types
9915
- *
9916
- * ```ts
9917
- * interface ColorPlugin extends Plugin {
9918
- * __type: "color"
9919
- * hex: string
9920
- * rgb: string
9921
- * }
9922
- *
9923
- * cms.plugins.add<ColorPlugin>({
9924
- * __type: "color",
9925
- * name: "red",
9926
- * hex: "#FF0000",
9927
- * rgb: "RGBA(255, 0, 0, 1)"
9928
- * })
9929
- * ```
9930
- *
9931
- * @typeparam P A subclass of Plugin. Optional.
9932
- * @param plugin
9933
- * @todo Consider returning the plugin which was just added.
9934
- */
9935
- add(plugin) {
9936
- this.findOrCreateMap(plugin.__type).add(plugin);
9937
- }
9938
- /**
9939
- * Removes the given plugin from the CMS.
9940
- *
9941
- * #### Example
9942
- *
9943
- * In this example a plugin is added to the CMS and removed
9944
- * 5 seconds later.
9945
- *
9946
- * ```ts
9947
- * const redPlugin = {
9948
- * __type: "color",
9949
- * name: "red",
9950
- * hex: "#FF0000",
9951
- * rgb: "RGBA(255, 0, 0, 1)"
9952
- * }
9953
- *
9954
- * cms.plugins.add(redPlugin)
9955
- *
9956
- * setTimeout(() => {
9957
- * cms.plugins.remove(redPlugin)
9958
- * }, 5000)
9959
- * ```
9960
- *
9961
- * @typeparam P A subclass of Plugin. Optional.
9962
- * @param plugin The plugin to be removed from the CMS.
9963
- */
9964
- remove(plugin) {
9965
- this.findOrCreateMap(plugin.__type).remove(plugin);
9966
- }
9967
- /**
9968
- * Returns a list of all the plugins of the given type.
9969
- *
9970
- * #### Example: Basic Usage
9971
- *
9972
- * ```ts
9973
- * cms.plugins.all("color").forEach(color => {
9974
- * console.log(color.name)
9975
- * })
9976
- * ```
9977
- *
9978
- * #### Example: Advanced Types
9979
- *
9980
- * A generic can be added to specify the type of plugins
9981
- * that are being retrieved.
9982
- *
9983
- * ```ts
9984
- * cms.plugins.all<ColorPlugin>("color").forEach(color => {
9985
- * console.log(color.name, color.hex)
9986
- * })
9987
- * ```
9988
- *
9989
- * @typeparam P A subclass of Plugin. Optional.
9990
- * @param type The name of the plugin
9991
- * @returns An array of all plugins of the given type.
9992
- */
9993
- all(type2) {
9994
- return this.findOrCreateMap(type2).all();
9995
- }
9996
- }
9997
- class PluginType {
9998
- /**
9999
- *
10000
- * @param __type The `__type` of plugin being managed.
10001
- */
10002
- constructor(__type, events) {
10003
- /**
10004
- * @ignore
10005
- */
10006
- __publicField(this, "__plugins", {});
10007
- this.__type = __type;
10008
- this.events = events;
10009
- }
10010
- /**
10011
- * Adds a new plugin to the collection.
10012
- *
10013
- * ### Example
10014
- *
10015
- * ```ts
10016
- * interface ColorPlugin extends Plugin {
10017
- * hex: string
10018
- * }
10019
- *
10020
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10021
- *
10022
- * colorPlugins.add({ name: "red", hex: "#f00" })
10023
- * ```
10024
- *
10025
- * @param plugin A new plugin. The `__type` is optional and will be added if it's missing.
10026
- */
10027
- add(plugin) {
10028
- const p = plugin;
10029
- if (!p.__type) {
10030
- p.__type = this.__type;
10031
- }
10032
- this.__plugins[p.name] = p;
10033
- this.events.dispatch({ type: `plugin:add:${this.__type}` });
10034
- }
10035
- all() {
10036
- return Object.keys(this.__plugins).map((name2) => this.__plugins[name2]);
10037
- }
10038
- /**
10039
- *
10040
- * Looks up a plugin by it's `name`.
10041
- *
10042
- * ### Example
10043
- *
10044
- * ```ts
10045
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10046
- *
10047
- * colorPlugins.add({ name: "red", hex: "#f00" })
10048
- *
10049
- * colorPlugins.find("red") // { __type: "color", name: "red", hex: "#f00" }
10050
- * colorPlugin.find("large") // undefined
10051
- * ```
10052
- *
10053
- * @param name The `name` of the plugin to be retrieved.
10054
- */
10055
- find(name2) {
10056
- return this.__plugins[name2];
10057
- }
10058
- /**
10059
- * Pass this function a plugin or the `name` of a plugin to have
10060
- * it be removed from the CMS.
10061
- *
10062
- * @param pluginOrName The `name` of a plugin, or the plugin itself.
10063
- * @returns The plugin that was removed, or `undefined` if it was not found.
10064
- */
10065
- remove(pluginOrName) {
10066
- const name2 = typeof pluginOrName === "string" ? pluginOrName : pluginOrName.name;
10067
- const plugin = this.__plugins[name2];
10068
- delete this.__plugins[name2];
10069
- this.events.dispatch({ type: `plugin:remove:${this.__type}` });
10070
- return plugin;
10071
- }
10072
- subscribe(cb) {
10073
- return this.events.subscribe(`plugin:*:${this.__type}`, cb);
10074
- }
10075
- }
10076
10141
  class EventBus {
10077
10142
  constructor() {
10078
10143
  __publicField(this, "listeners", /* @__PURE__ */ new Set());
@@ -10151,6 +10216,19 @@ class Listener {
10151
10216
  return !ignoresEvent;
10152
10217
  }
10153
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
+ }
10154
10232
  const EDITORIAL_WORKFLOW_STATUS = {
10155
10233
  QUEUED: "queued",
10156
10234
  PROCESSING: "processing",
@@ -10238,6 +10316,26 @@ const MEDIA_WORKFLOW_STATUS_TO_STEP = {
10238
10316
  [EDITORIAL_WORKFLOW_STATUS.COMPLETE]: MEDIA_WORKFLOW_STEP.COMPLETE
10239
10317
  };
10240
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
+ }
10241
10339
  class DummyMediaStore {
10242
10340
  constructor() {
10243
10341
  __publicField(this, "accept", "*");
@@ -10279,10 +10377,9 @@ class TinaMediaStore {
10279
10377
  __publicField(this, "workflowBranchOverride");
10280
10378
  __publicField(this, "mediaWorkflowInProgress", false);
10281
10379
  /**
10282
- * Renaming is only implemented against the local dev server. This is an
10283
- * instance property rather than a prototype method on purpose: the media
10380
+ * An instance property rather than a prototype method on purpose: the media
10284
10381
  * manager decides whether to offer the action by checking whether the store
10285
- * 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
10286
10383
  * prototype method would advertise support everywhere and surface an action
10287
10384
  * that always fails.
10288
10385
  */
@@ -10300,8 +10397,8 @@ class TinaMediaStore {
10300
10397
  this.isStatic = true;
10301
10398
  this.staticMedia = staticMedia;
10302
10399
  }
10303
- if (!this.isStatic && ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode)) {
10304
- 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);
10305
10402
  }
10306
10403
  }
10307
10404
  /**
@@ -10347,27 +10444,29 @@ class TinaMediaStore {
10347
10444
  return await this.api.authProvider.isAuthenticated();
10348
10445
  }
10349
10446
  /**
10350
- * Returns the workflow branch override or current branch as a single-encoded
10351
- * query-param value, or an empty string when no branch is set.
10352
- *
10353
- * `this.api.branch` is already URL-encoded by `Client.setBranch()`, so we
10354
- * decode then re-encode here to defend against double-encoding when this
10355
- * value is concatenated into a URL.
10356
- *
10357
- * `Client.setBranch()` runs the constructor's `options.branch` through
10358
- * `encodeURIComponent` without a guard, so an unset `options.branch`
10359
- * lands here as the literal string `"undefined"`. We treat that and the
10360
- * empty case as no-branch so we don't send `?branch=undefined` to the
10361
- * 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.
10362
10452
  */
10363
- encodedBranchParam() {
10364
- if (this.workflowBranchOverride) {
10365
- return encodeURIComponent(this.workflowBranchOverride);
10366
- }
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;
10367
10462
  if (!this.api.branch) return "";
10368
10463
  const decoded = decodeURIComponent(this.api.branch);
10369
- if (!decoded || decoded === "undefined") return "";
10370
- 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) : "";
10371
10470
  }
10372
10471
  shortStableHash(input) {
10373
10472
  let hash = 2166136261;
@@ -10405,7 +10504,7 @@ class TinaMediaStore {
10405
10504
  const encodedBranch = this.encodedBranchParam();
10406
10505
  return encodedBranch ? `?branch=${encodedBranch}` : "";
10407
10506
  }
10408
- requestMediaBranchChoice(branchName, baseBranch, opType, repoPath) {
10507
+ requestMediaBranchChoice(branchName, baseBranch, request) {
10409
10508
  if (!this.cms.events.hasExplicitListenerFor("media:workflow:confirm-branch")) {
10410
10509
  throw new Error(
10411
10510
  "Cannot start a media editorial workflow: no branch prompt is mounted. Ensure <MediaWorkflowOverlay /> is rendered (TinaCloudProvider mounts it automatically)."
@@ -10416,12 +10515,13 @@ class TinaMediaStore {
10416
10515
  type: "media:workflow:confirm-branch",
10417
10516
  branchName,
10418
10517
  baseBranch,
10518
+ // A rename on a protected branch can only go through the workflow.
10519
+ allowSaveToProtectedBranch: request.opType !== "rename",
10419
10520
  onConfirm: async (selectedBranchName) => {
10420
10521
  const context = await this.prepareMediaBranch(
10421
10522
  selectedBranchName,
10422
10523
  baseBranch,
10423
- opType,
10424
- repoPath
10524
+ request
10425
10525
  );
10426
10526
  resolve({
10427
10527
  kind: "workflow",
@@ -10433,7 +10533,7 @@ class TinaMediaStore {
10433
10533
  });
10434
10534
  });
10435
10535
  }
10436
- async prepareMediaBranch(branchName, baseBranch, opType, repoPath) {
10536
+ async prepareMediaBranch(branchName, baseBranch, request) {
10437
10537
  if (this.mediaWorkflowInProgress) {
10438
10538
  throw new Error("A media workflow is already in progress.");
10439
10539
  }
@@ -10448,8 +10548,9 @@ class TinaMediaStore {
10448
10548
  branchName,
10449
10549
  baseBranch,
10450
10550
  prTitle: getEditorialWorkflowPrTitle(branchName),
10451
- operation: opType,
10452
- repoPath
10551
+ operation: request.opType,
10552
+ repoPath: request.repoPath,
10553
+ targetRepoPath: request.targetRepoPath
10453
10554
  });
10454
10555
  const branchContext = {
10455
10556
  branchName: workflow.branchName || branchName,
@@ -10525,12 +10626,10 @@ class TinaMediaStore {
10525
10626
  const branchName = `media-${opType}-${mediaSlug}`;
10526
10627
  const repoFilename = opType === "upload" && filename ? sanitizeFilename(filename) : filename;
10527
10628
  const repoPath = this.joinMediaPath(directory, repoFilename);
10528
- return this.requestMediaBranchChoice(
10529
- branchName,
10530
- baseBranch,
10629
+ return this.requestMediaBranchChoice(branchName, baseBranch, {
10531
10630
  opType,
10532
10631
  repoPath
10533
- );
10632
+ });
10534
10633
  }
10535
10634
  async waitForRequestStatus(requestId, timeoutMessage) {
10536
10635
  const startTime = Date.now();
@@ -10794,6 +10893,160 @@ class TinaMediaStore {
10794
10893
  lastSlash === -1 ? to2 : to2.slice(lastSlash + 1)
10795
10894
  );
10796
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
+ }
10797
11050
  toRenameError(status, body) {
10798
11051
  if (body == null ? void 0 : body.code) {
10799
11052
  return new MediaRenameError({
@@ -10821,6 +11074,7 @@ class TinaMediaStore {
10821
11074
  return !this.isLocal ? `${src}?fit=crop&max-w=${dimensions.w}&max-h=${dimensions.h}` : src;
10822
11075
  }
10823
11076
  async list(options) {
11077
+ var _a, _b;
10824
11078
  this.setup();
10825
11079
  if (this.staticMedia) {
10826
11080
  const offset2 = options.offset || 0;
@@ -10852,13 +11106,16 @@ class TinaMediaStore {
10852
11106
  nextOffset: hasMore2 ? Number(offset2) + 20 : null
10853
11107
  };
10854
11108
  }
10855
- return { items: media, nextOffset: hasMore ? Number(offset2) + 20 : null };
11109
+ return {
11110
+ items: media,
11111
+ nextOffset: hasMore ? Number(offset2) + 20 : null
11112
+ };
10856
11113
  }
10857
11114
  let res;
10858
11115
  if (!this.isLocal) {
10859
11116
  const encodedBranch = this.encodedBranchParam();
10860
11117
  res = await this.api.authProvider.fetchWithToken(
10861
- `${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(","))}` : ""}`
10862
11119
  );
10863
11120
  if (res.status == 401) {
10864
11121
  throw E_UNAUTHORIZED;
@@ -10868,7 +11125,7 @@ class TinaMediaStore {
10868
11125
  }
10869
11126
  } else {
10870
11127
  res = await this.fetchFunction(
10871
- `${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(","))}` : ""}`
10872
11129
  );
10873
11130
  if (res.status == 404) {
10874
11131
  throw E_BAD_ROUTE;
@@ -11113,17 +11370,217 @@ const E_SELF_HOSTED_MEDIA = new MediaListError({
11113
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.",
11114
11371
  docsLink: "https://tina.io/docs/r/backend-media-handler/"
11115
11372
  });
11116
- class Flags {
11373
+ class PluginTypeManager {
11117
11374
  constructor(events) {
11118
- __publicField(this, "_flags", /* @__PURE__ */ new Map());
11375
+ /**
11376
+ * @ignore
11377
+ */
11378
+ __publicField(this, "plugins", {});
11119
11379
  this.events = events;
11120
11380
  }
11121
- get(key) {
11122
- 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);
11123
11404
  }
11124
- set(key, value) {
11125
- this._flags.set(key, value);
11126
- 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);
11127
11584
  }
11128
11585
  }
11129
11586
  const _CMS = class _CMS {
@@ -11236,14 +11693,28 @@ const _CMS = class _CMS {
11236
11693
  this.unsubscribeHooks[name2]();
11237
11694
  }
11238
11695
  if (api.events instanceof EventBus) {
11239
- const unsubscribeHost = api.events.subscribe(
11240
- "*",
11241
- this.events.dispatch
11242
- );
11243
- const unsubscribeGuest = this.events.subscribe(
11244
- "*",
11245
- (e) => api.events.dispatch(e)
11246
- );
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
+ });
11247
11718
  this.unsubscribeHooks[name2] = () => {
11248
11719
  unsubscribeHost();
11249
11720
  unsubscribeGuest();
@@ -11580,13 +12051,16 @@ const RenameModal$1 = ({
11580
12051
  variant: "primary",
11581
12052
  disabled: !canSubmit,
11582
12053
  onClick: async () => {
11583
- if (!canSubmit) return;
11584
12054
  setProcessing(true);
11585
12055
  setError(null);
11586
12056
  try {
11587
12057
  await renameFunc(sanitized);
11588
12058
  close2();
11589
12059
  } catch (e) {
12060
+ if ((e == null ? void 0 : e.ERR_TYPE) === "MediaRenameCancelled") {
12061
+ close2();
12062
+ return;
12063
+ }
11590
12064
  setError(renameErrorMessage(e, sanitized));
11591
12065
  setProcessing(false);
11592
12066
  }
@@ -11696,6 +12170,7 @@ function MediaPicker({
11696
12170
  allowDelete,
11697
12171
  onSelect,
11698
12172
  close: close2,
12173
+ accept: fieldAccept,
11699
12174
  ...props
11700
12175
  }) {
11701
12176
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
@@ -11732,6 +12207,7 @@ function MediaPicker({
11732
12207
  const [mediaFilter, setMediaFilter] = useState(
11733
12208
  "all"
11734
12209
  );
12210
+ const [typeFilter, setTypeFilter] = useState("all");
11735
12211
  const [offsetHistory, setOffsetHistory] = useState([]);
11736
12212
  const offset2 = offsetHistory[offsetHistory.length - 1];
11737
12213
  const resetOffset = () => setOffsetHistory([]);
@@ -11744,8 +12220,25 @@ function MediaPicker({
11744
12220
  resetList();
11745
12221
  setActiveItem(false);
11746
12222
  };
12223
+ const changeTypeFilter = (next) => {
12224
+ if (next === typeFilter) return;
12225
+ setTypeFilter(next);
12226
+ setLoadFolders(true);
12227
+ resetOffset();
12228
+ resetList();
12229
+ setActiveItem(false);
12230
+ };
11747
12231
  const listRequestRef = useRef(0);
11748
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]);
11749
12242
  async function loadMedia(loadFolders2 = true) {
11750
12243
  const requestId = ++listRequestRef.current;
11751
12244
  setListState("loading");
@@ -11760,7 +12253,8 @@ function MediaPicker({
11760
12253
  { w: 1e3, h: 1e3 }
11761
12254
  ],
11762
12255
  filesOnly: !loadFolders2,
11763
- search: debouncedSearch || void 0
12256
+ search: debouncedSearch || void 0,
12257
+ ext: acceptedExtensions.length ? acceptedExtensions : void 0
11764
12258
  });
11765
12259
  if (requestId !== listRequestRef.current) return;
11766
12260
  setList((prev) => {
@@ -11803,9 +12297,6 @@ function MediaPicker({
11803
12297
  }, [refreshing]);
11804
12298
  useEffect(() => {
11805
12299
  if (!cms.media.isConfigured) return;
11806
- if (refreshing) return;
11807
- loadMedia(loadFolders);
11808
- if (loadFolders) setLoadFolders(false);
11809
12300
  return cms.events.subscribe(
11810
12301
  ["media:delete:success", "media:rename:success", "media:pageSize"],
11811
12302
  (event) => {
@@ -11819,7 +12310,19 @@ function MediaPicker({
11819
12310
  resetList();
11820
12311
  }
11821
12312
  );
11822
- }, [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
+ ]);
11823
12326
  const onClickMediaItem = (item) => {
11824
12327
  if (!item) {
11825
12328
  setActiveItem(false);
@@ -11853,12 +12356,12 @@ function MediaPicker({
11853
12356
  };
11854
12357
  }
11855
12358
  const [uploading, setUploading] = useState(false);
11856
- const accept = Array.isArray(
12359
+ const globalAccept = Array.isArray(
11857
12360
  (_c = (_b = (_a = cms.api.tina.schema.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.media) == null ? void 0 : _c.accept
11858
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;
11859
12362
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
11860
- accept: dropzoneAcceptFromString(
11861
- accept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
12363
+ accept: dropzoneAcceptFromExtensions(fieldExtensions) ?? dropzoneAcceptFromString(
12364
+ globalAccept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
11862
12365
  ),
11863
12366
  maxSize: cms.media.maxSize,
11864
12367
  multiple: true,
@@ -12075,7 +12578,15 @@ function MediaPicker({
12075
12578
  setValue: setSearch,
12076
12579
  clear: () => setSearch("")
12077
12580
  }
12078
- ), /* @__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(
12079
12590
  MediaFilterToggle,
12080
12591
  {
12081
12592
  value: mediaFilter,
@@ -12340,6 +12851,62 @@ const MediaFilterToggle = ({
12340
12851
  option.label
12341
12852
  )));
12342
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
+ };
12343
12910
  const ViewModeToggle = ({ viewMode, setViewMode }) => {
12344
12911
  const base = "relative flex items-center justify-center w-9 py-1.5 transition-colors ease-out duration-150";
12345
12912
  const active = "bg-white text-tina-orange";
@@ -13591,7 +14158,7 @@ const FormModal = ({ plugin, close: close2 }) => {
13591
14158
  const name = "tinacms";
13592
14159
  const type = "module";
13593
14160
  const typings = "dist/index.d.ts";
13594
- const version = "3.12.0";
14161
+ const version = "3.13.0";
13595
14162
  const main = "dist/index.js";
13596
14163
  const module$1 = "./dist/index.js";
13597
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" };
@@ -13599,8 +14166,8 @@ const files = ["dist"];
13599
14166
  const buildConfig = { "entryPoints": ["src/index.ts", "src/rich-text/index.tsx", "src/rich-text/static.tsx", "src/rich-text/prism.tsx", "src/react.tsx", "src/tina-field.ts", { "name": "src/client.ts", "target": "node" }] };
13600
14167
  const license = "Apache-2.0";
13601
14168
  const scripts = { "types": "pnpm tsc && pnpm tsc-alias", "build": "tinacms-scripts build", "test": "vitest run", "test-watch": "vitest" };
13602
- const dependencies = { "@ariakit/react": "catalog:", "@dnd-kit/core": "catalog:", "@dnd-kit/sortable": "catalog:", "@dnd-kit/utilities": "catalog:", "@floating-ui/dom": "catalog:", "@floating-ui/react-dom": "catalog:", "@graphql-inspector/core": "^6.2.1", "@radix-ui/react-dialog": "catalog:", "@radix-ui/react-dropdown-menu": "catalog:", "@radix-ui/react-popover": "catalog:", "@radix-ui/react-select": "catalog:", "@radix-ui/react-separator": "catalog:", "@radix-ui/react-slot": "catalog:", "@radix-ui/react-toolbar": "catalog:", "@radix-ui/react-tooltip": "catalog:", "@react-hook/window-size": "catalog:", "@tanstack/react-table": "^8.21.3", "@tinacms/bridge": "workspace:^", "@tinacms/mdx": "workspace:^", "@tinacms/schema-tools": "workspace:^", "@tinacms/search": "workspace:^", "@udecode/cmdk": "catalog:", "@udecode/cn": "catalog:", "@udecode/plate": "catalog:", "@udecode/plate-autoformat": "catalog:", "@udecode/plate-basic-marks": "catalog:", "@udecode/plate-block-quote": "catalog:", "@udecode/plate-break": "catalog:", "@udecode/plate-code-block": "catalog:", "@udecode/plate-combobox": "catalog:", "@udecode/plate-dnd": "catalog:", "@udecode/plate-floating": "catalog:", "@udecode/plate-heading": "catalog:", "@udecode/plate-highlight": "catalog:", "@udecode/plate-horizontal-rule": "catalog:", "@udecode/plate-indent-list": "catalog:", "@udecode/plate-link": "catalog:", "@udecode/plate-list": "catalog:", "@udecode/plate-node-id": "catalog:", "@udecode/plate-reset-node": "catalog:", "@udecode/plate-resizable": "catalog:", "@udecode/plate-selection": "catalog:", "@udecode/plate-slash-command": "catalog:", "@udecode/plate-table": "catalog:", "@udecode/plate-trailing-block": "catalog:", "class-variance-authority": "catalog:", "clsx": "catalog:", "cmdk": "catalog:", "color-string": "catalog:", "crypto-js": "catalog:", "date-fns": "4.1.0", "es-toolkit": "^1.42.0", "final-form": "catalog:", "final-form-arrays": "catalog:", "final-form-set-field-data": "catalog:", "graphql": "15.8.0", "graphql-tag": "catalog:", "is-hotkey": "catalog:", "lucide-react": "catalog:", "mermaid": "catalog:", "posthog-js": "^1.347.1", "prism-react-renderer": "catalog:", "prop-types": "catalog:", "react-colorful": "catalog:", "react-day-picker": "^9.13.0", "react-dnd": "catalog:", "react-dnd-html5-backend": "catalog:", "react-dropzone": "catalog:", "react-final-form": "catalog:", "react-router-dom": "catalog:", "react-use": "catalog:", "tailwind-merge": "^2.6.0", "webfontloader": "catalog:", "yup": "^1.6.1", "zod": "catalog:" };
13603
- const devDependencies = { "@graphql-tools/utils": "catalog:", "@testing-library/dom": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@testing-library/user-event": "catalog:", "@tinacms/scripts": "workspace:*", "@types/atob": "catalog:", "@types/codemirror": "catalog:", "@types/color-string": "catalog:", "@types/node": "^22.13.1", "@types/prop-types": "catalog:", "@types/react": "^18.3.18", "@types/react-dom": "catalog:", "happy-dom": "catalog:", "identity-obj-proxy": "catalog:", "jest-file-snapshot": "^0.7.0", "lowlight": "catalog:", "next": "14.2.35", "react": "^18.3.1", "react-dom": "^18.3.1", "react-is": "^18.3.1", "tsc-alias": "catalog:", "tslib": "catalog:", "typescript": "^5.7.3", "vite": "^5.4.14", "vitest": "^2.1.9" };
14169
+ const dependencies = { "@ariakit/react": "catalog:", "@dnd-kit/core": "catalog:", "@dnd-kit/sortable": "catalog:", "@dnd-kit/utilities": "catalog:", "@floating-ui/dom": "catalog:", "@floating-ui/react-dom": "catalog:", "@graphql-inspector/core": "^6.2.1", "@radix-ui/react-dialog": "catalog:", "@radix-ui/react-dropdown-menu": "catalog:", "@radix-ui/react-popover": "catalog:", "@radix-ui/react-select": "catalog:", "@radix-ui/react-separator": "catalog:", "@radix-ui/react-slot": "catalog:", "@radix-ui/react-toolbar": "catalog:", "@radix-ui/react-tooltip": "catalog:", "@react-hook/window-size": "catalog:", "@tanstack/react-table": "^8.21.3", "@tinacms/bridge": "workspace:^", "@tinacms/mdx": "workspace:^", "@tinacms/schema-tools": "workspace:^", "@tinacms/search": "workspace:^", "@udecode/cmdk": "catalog:", "@udecode/cn": "catalog:", "@udecode/plate": "catalog:", "@udecode/plate-autoformat": "catalog:", "@udecode/plate-basic-marks": "catalog:", "@udecode/plate-block-quote": "catalog:", "@udecode/plate-break": "catalog:", "@udecode/plate-code-block": "catalog:", "@udecode/plate-combobox": "catalog:", "@udecode/plate-dnd": "catalog:", "@udecode/plate-floating": "catalog:", "@udecode/plate-heading": "catalog:", "@udecode/plate-highlight": "catalog:", "@udecode/plate-horizontal-rule": "catalog:", "@udecode/plate-indent-list": "catalog:", "@udecode/plate-link": "catalog:", "@udecode/plate-list": "catalog:", "@udecode/plate-node-id": "catalog:", "@udecode/plate-reset-node": "catalog:", "@udecode/plate-resizable": "catalog:", "@udecode/plate-selection": "catalog:", "@udecode/plate-slash-command": "catalog:", "@udecode/plate-table": "catalog:", "@udecode/plate-trailing-block": "catalog:", "class-variance-authority": "catalog:", "clsx": "catalog:", "cmdk": "catalog:", "color-string": "catalog:", "date-fns": "4.1.0", "es-toolkit": "^1.42.0", "final-form": "catalog:", "final-form-arrays": "catalog:", "final-form-set-field-data": "catalog:", "graphql": "15.8.0", "graphql-tag": "catalog:", "is-hotkey": "catalog:", "lucide-react": "catalog:", "mermaid": "catalog:", "posthog-js": "^1.347.1", "prism-react-renderer": "catalog:", "prop-types": "catalog:", "react-colorful": "catalog:", "react-day-picker": "^9.13.0", "react-dnd": "catalog:", "react-dnd-html5-backend": "catalog:", "react-dropzone": "catalog:", "react-final-form": "catalog:", "react-router-dom": "catalog:", "react-use": "catalog:", "tailwind-merge": "^2.6.0", "webfontloader": "catalog:", "yup": "^1.6.1", "zod": "catalog:" };
14170
+ const devDependencies = { "@graphql-tools/utils": "catalog:", "@testing-library/dom": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@testing-library/user-event": "catalog:", "@tinacms/scripts": "workspace:*", "@types/atob": "catalog:", "@types/codemirror": "catalog:", "@types/color-string": "catalog:", "@types/node": "^22.13.1", "@types/prop-types": "catalog:", "@types/react": "^18.3.18", "@types/react-dom": "catalog:", "happy-dom": "catalog:", "identity-obj-proxy": "catalog:", "jest-file-snapshot": "^0.7.0", "lowlight": "catalog:", "next": "15.5.21", "react": "^18.3.1", "react-dom": "^18.3.1", "react-is": "^18.3.1", "tsc-alias": "catalog:", "tslib": "catalog:", "typescript": "^5.7.3", "vite": "^5.4.14", "vitest": "^2.1.9" };
13604
14171
  const peerDependencies = { "react": ">=16.14.0", "react-dom": ">=16.14.0" };
13605
14172
  const publishConfig = { "registry": "https://registry.npmjs.org" };
13606
14173
  const repository = { "url": "https://github.com/tinacms/tinacms.git", "directory": "packages/tinacms" };
@@ -13699,7 +14266,11 @@ const useGetEvents = (cms, cursor, existingEvents) => {
13699
14266
  var _a, _b, _c, _d, _e;
13700
14267
  let doFetchEvents = false;
13701
14268
  if (!((_b = (_a = cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isCustomContentApi)) {
13702
- 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
+ }
13703
14274
  }
13704
14275
  if (doFetchEvents) {
13705
14276
  try {
@@ -13732,7 +14303,11 @@ function useSyncStatus(cms) {
13732
14303
  var _a2, _b2, _c, _d, _e;
13733
14304
  let doFetchEvents = false;
13734
14305
  if (!((_b2 = (_a2 = cms.api) == null ? void 0 : _a2.tina) == null ? void 0 : _b2.isCustomContentApi)) {
13735
- 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
+ }
13736
14311
  }
13737
14312
  if (doFetchEvents) {
13738
14313
  const { events } = await cms.api.tina.fetchEvents();
@@ -16256,34 +16831,6 @@ const BranchPreviewButton = (props) => {
16256
16831
  /* @__PURE__ */ React.createElement(ExternalLink, { className: "h-5 w-auto" })
16257
16832
  );
16258
16833
  };
16259
- function useLocalStorage(key, initialValue) {
16260
- const [storedValue, setStoredValue] = React.useState(() => {
16261
- try {
16262
- const valueFromStorage = typeof window !== "undefined" && window.localStorage && window.localStorage.getItem(key);
16263
- if (valueFromStorage != null) {
16264
- return JSON.parse(valueFromStorage);
16265
- }
16266
- } catch (error) {
16267
- }
16268
- return initialValue;
16269
- });
16270
- React.useEffect(() => {
16271
- const valueFromStorage = window.localStorage && window.localStorage.getItem(key);
16272
- if (valueFromStorage != null && valueFromStorage != void 0) {
16273
- setStoredValue(JSON.parse(valueFromStorage));
16274
- }
16275
- }, [key]);
16276
- const setValue = (value) => {
16277
- try {
16278
- const valueToStore = value instanceof Function ? value(storedValue) : value;
16279
- setStoredValue(valueToStore);
16280
- localStorage.setItem(key, JSON.stringify(valueToStore));
16281
- } catch (error) {
16282
- console.log(error);
16283
- }
16284
- };
16285
- return [storedValue, setValue];
16286
- }
16287
16834
  const CREATE_DOCUMENT_GQL = `#graphql
16288
16835
  mutation($collection: String!, $relativePath: String!, $params: DocumentMutation!) {
16289
16836
  createDocument(
@@ -16923,22 +17470,6 @@ const resolveSaveOptions = (lastChoice, disablePublish) => {
16923
17470
  const menu = SAVE_CHOICE_ORDER.filter((choice) => choice !== main2);
16924
17471
  return { main: main2, menu };
16925
17472
  };
16926
- const formatDefaultBranchName = (filePath, crudType) => {
16927
- let result = filePath;
16928
- const contentPrefix = "content/";
16929
- if (result.startsWith(contentPrefix)) {
16930
- result = result.substring(contentPrefix.length);
16931
- }
16932
- const lastDot = result.lastIndexOf(".");
16933
- const lastSlash = Math.max(result.lastIndexOf("/"), result.lastIndexOf("\\"));
16934
- if (lastDot > lastSlash && lastDot > 0) {
16935
- result = result.slice(0, lastDot);
16936
- }
16937
- if (crudType === "delete") {
16938
- result = `❌-${result}`;
16939
- }
16940
- return result;
16941
- };
16942
17473
  const CreateBranchModal = ({
16943
17474
  close: close2,
16944
17475
  safeSubmit,
@@ -16954,6 +17485,7 @@ const CreateBranchModal = ({
16954
17485
  formatDefaultBranchName(path, crudType)
16955
17486
  );
16956
17487
  const [isBranchGuardChecking, setIsBranchGuardChecking] = React.useState(false);
17488
+ const normalizedBranchName = normalizeBranchName(newBranchName);
16957
17489
  const branchGuardAbortRef = React.useRef(null);
16958
17490
  const {
16959
17491
  isExecuting,
@@ -16981,7 +17513,7 @@ const CreateBranchModal = ({
16981
17513
  branchGuardAbortRef.current = abortController;
16982
17514
  setIsBranchGuardChecking(true);
16983
17515
  const baseBranch = decodeURIComponent(tinaApi.branch);
16984
- const targetBranch = `tina/${newBranchName}`;
17516
+ const targetBranch = `tina/${normalizedBranchName}`;
16985
17517
  const { baseBranchExists, targetBranchExists } = await checkBranchGuard(
16986
17518
  tinaApi,
16987
17519
  baseBranch,
@@ -17042,7 +17574,7 @@ const CreateBranchModal = ({
17042
17574
  close2();
17043
17575
  },
17044
17576
  errorMessage,
17045
- disabled: newBranchName === "" || isBranchGuardChecking,
17577
+ disabled: normalizedBranchName === "" || isBranchGuardChecking,
17046
17578
  onBranchNameChange: (value) => {
17047
17579
  abortBranchGuard();
17048
17580
  reset();
@@ -17059,6 +17591,35 @@ const CreateBranchModal = ({
17059
17591
  }
17060
17592
  );
17061
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
+ };
17062
17623
  const CreateBranchPromptModal = ({
17063
17624
  branchName,
17064
17625
  close: close2,
@@ -17068,7 +17629,8 @@ const CreateBranchPromptModal = ({
17068
17629
  onCreateBranch,
17069
17630
  onSaveToProtectedBranch,
17070
17631
  showSaveOptions = false,
17071
- disablePublish = false
17632
+ disablePublish = false,
17633
+ allowSaveToProtectedBranch = true
17072
17634
  }) => {
17073
17635
  const [lastChoice, setLastChoice] = useLocalStorage(
17074
17636
  SAVE_CHOICE_KEY,
@@ -17115,7 +17677,7 @@ const CreateBranchPromptModal = ({
17115
17677
  onBranchNameChange(e.target.value);
17116
17678
  }
17117
17679
  }
17118
- ))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17680
+ ), /* @__PURE__ */ React.createElement(CommittingAs, null))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17119
17681
  Button$2,
17120
17682
  {
17121
17683
  variant: "secondary",
@@ -17148,7 +17710,7 @@ const CreateBranchPromptModal = ({
17148
17710
  },
17149
17711
  /* @__PURE__ */ React.createElement(MainIcon, { className: "w-4 h-4 mr-1", style: { fill: "none" } }),
17150
17712
  mainChoice.label
17151
- ) : /* @__PURE__ */ React.createElement(
17713
+ ) : allowSaveToProtectedBranch ? /* @__PURE__ */ React.createElement(
17152
17714
  DropdownButton,
17153
17715
  {
17154
17716
  variant: "primary",
@@ -17172,6 +17734,25 @@ const CreateBranchPromptModal = ({
17172
17734
  }
17173
17735
  ),
17174
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
+ )
17175
17756
  ))));
17176
17757
  };
17177
17758
  const PrefixedTextField = ({
@@ -17200,6 +17781,7 @@ const BranchDeletedModal = ({
17200
17781
  const cms = useCMS$1();
17201
17782
  const tinaApi = cms.api.tina;
17202
17783
  const [newBranchName, setNewBranchName] = React.useState("");
17784
+ const normalizedBranchName = normalizeBranchName(newBranchName);
17203
17785
  const baseBranch = tinaApi.protectedBranches[0] || cms.api.tina.schema.config.config.repoProvider.defaultBranchName || "main";
17204
17786
  const {
17205
17787
  isExecuting,
@@ -17211,7 +17793,7 @@ const BranchDeletedModal = ({
17211
17793
  } = useEditorialWorkflow();
17212
17794
  const handleCreate = async () => {
17213
17795
  const { success } = await executeWorkflow({
17214
- branchName: `tina/${newBranchName}`,
17796
+ branchName: `tina/${normalizedBranchName}`,
17215
17797
  baseBranch,
17216
17798
  path,
17217
17799
  values,
@@ -17258,7 +17840,7 @@ const BranchDeletedModal = ({
17258
17840
  {
17259
17841
  variant: "primary",
17260
17842
  className: "w-full sm:w-auto",
17261
- disabled: !newBranchName,
17843
+ disabled: !normalizedBranchName,
17262
17844
  onClick: handleCreate
17263
17845
  },
17264
17846
  /* @__PURE__ */ React.createElement(
@@ -17523,6 +18105,10 @@ const FormBuilder = ({
17523
18105
  setDeletedBranchModalOpen(true);
17524
18106
  return;
17525
18107
  }
18108
+ if (isSessionExpiredError(error)) {
18109
+ captureWorkflowChoice(false, errorMsg);
18110
+ return;
18111
+ }
17526
18112
  captureEvent(SaveContentErrorEvent, {
17527
18113
  documentPath: tinaForm.path,
17528
18114
  error: errorMsg
@@ -18623,7 +19209,7 @@ function HeadingsMenu(props) {
18623
19209
  className: "min-w-0 rounded-none rounded-bl rounded-br border-border"
18624
19210
  },
18625
19211
  /* @__PURE__ */ React__default.createElement(
18626
- DropdownMenuRadioGroup,
19212
+ DropdownMenuRadioGroup$1,
18627
19213
  {
18628
19214
  className: "flex flex-col gap-0.5",
18629
19215
  onValueChange: (type2) => {
@@ -18639,7 +19225,7 @@ function HeadingsMenu(props) {
18639
19225
  }
18640
19226
  return true;
18641
19227
  }).map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
18642
- DropdownMenuRadioItem,
19228
+ DropdownMenuRadioItem$1,
18643
19229
  {
18644
19230
  className: "min-w-[180px]",
18645
19231
  key: itemValue,
@@ -19395,549 +19981,6 @@ const useCreateEditor = ({
19395
19981
  components
19396
19982
  });
19397
19983
  };
19398
- const popoverVariants = cva(
19399
- "z-[999999] w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
19400
- );
19401
- const inputVariants = cva(
19402
- "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"
19403
- );
19404
- function LinkFloatingToolbar({
19405
- state
19406
- }) {
19407
- const activeCommentId = usePluginOption({ key: "comment" }, "activeId");
19408
- const activeSuggestionId = usePluginOption({ key: "suggestion" }, "activeId");
19409
- const { api, editor } = useEditorPlugin(LinkPlugin);
19410
- const isUrlValidator = usePluginOption(LinkPlugin, "isUrl");
19411
- const [currentUrl, setCurrentUrl] = React.useState("");
19412
- const [isValidUrl, setIsValidUrl] = React.useState(true);
19413
- const handleUrlInput = React.useCallback(
19414
- (e) => {
19415
- const value = e.currentTarget.value;
19416
- setCurrentUrl(value);
19417
- if (value && isUrlValidator) {
19418
- const valid = isUrlValidator(value);
19419
- setIsValidUrl(valid);
19420
- } else {
19421
- setIsValidUrl(true);
19422
- }
19423
- },
19424
- [isUrlValidator]
19425
- );
19426
- const floatingOptions = React.useMemo(() => {
19427
- return {
19428
- middleware: [
19429
- offset(8),
19430
- flip({
19431
- fallbackPlacements: ["bottom-end", "top-start", "top-end"],
19432
- padding: 12
19433
- }),
19434
- shift({ padding: 8, limiter: limitShift() })
19435
- ],
19436
- placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start"
19437
- };
19438
- }, [activeCommentId, activeSuggestionId]);
19439
- const insertState = useFloatingLinkInsertState({
19440
- ...state,
19441
- floatingOptions: {
19442
- ...floatingOptions,
19443
- ...state == null ? void 0 : state.floatingOptions
19444
- }
19445
- });
19446
- const {
19447
- hidden,
19448
- props: insertProps,
19449
- ref: insertRef,
19450
- textInputProps
19451
- } = useFloatingLinkInsert(insertState);
19452
- const editState = useFloatingLinkEditState({
19453
- ...state,
19454
- floatingOptions: {
19455
- ...floatingOptions,
19456
- ...state == null ? void 0 : state.floatingOptions
19457
- }
19458
- });
19459
- const {
19460
- editButtonProps,
19461
- props: editProps,
19462
- ref: editRef,
19463
- unlinkButtonProps
19464
- } = useFloatingLinkEdit(editState);
19465
- const inputProps = useFormInputProps({
19466
- preventDefaultOnEnterKeydown: true
19467
- });
19468
- if (hidden) return null;
19469
- const input = /* @__PURE__ */ React.createElement(
19470
- "div",
19471
- {
19472
- className: "z-[999999] flex w-[330px] flex-col relative",
19473
- ...inputProps
19474
- },
19475
- !isValidUrl && currentUrl && /* @__PURE__ */ React.createElement("div", { className: "absolute -top-16 left-0 right-0 z-[1000000] mb-2" }, /* @__PURE__ */ React.createElement(
19476
- "div",
19477
- {
19478
- className: "bg-red-50 border border-red-200 rounded-md p-2 shadow-lg",
19479
- role: "alert",
19480
- "aria-live": "polite"
19481
- },
19482
- /* @__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")),
19483
- /* @__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" })
19484
- )),
19485
- /* @__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(
19486
- FloatingLinkUrlInput,
19487
- {
19488
- className: inputVariants(),
19489
- placeholder: "Paste link",
19490
- "data-plate-focus": true,
19491
- onInput: handleUrlInput
19492
- }
19493
- )),
19494
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19495
- /* @__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(
19496
- "input",
19497
- {
19498
- className: inputVariants(),
19499
- placeholder: "Text to display",
19500
- "data-plate-focus": true,
19501
- ...textInputProps
19502
- }
19503
- )),
19504
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19505
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-2 px-2 py-1" }, /* @__PURE__ */ React.createElement(
19506
- "button",
19507
- {
19508
- type: "button",
19509
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19510
- onClick: () => {
19511
- api.floatingLink.hide();
19512
- }
19513
- },
19514
- "Cancel"
19515
- ), /* @__PURE__ */ React.createElement(
19516
- "button",
19517
- {
19518
- type: "button",
19519
- className: buttonVariants$2({
19520
- size: "sm",
19521
- variant: "tinaPrimary"
19522
- }),
19523
- onClick: () => {
19524
- if (isValidUrl && currentUrl) {
19525
- submitFloatingLink(editor);
19526
- }
19527
- },
19528
- disabled: !isValidUrl && !!currentUrl
19529
- },
19530
- "OK"
19531
- ))
19532
- );
19533
- const editContent = editState.isEditing ? input : /* @__PURE__ */ React.createElement("div", { className: "box-content flex items-center" }, /* @__PURE__ */ React.createElement(
19534
- "button",
19535
- {
19536
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19537
- type: "button",
19538
- ...editButtonProps
19539
- },
19540
- "Edit link"
19541
- ), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(LinkOpenButton, null), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(
19542
- "button",
19543
- {
19544
- className: buttonVariants$2({
19545
- size: "icon",
19546
- variant: "ghost"
19547
- }),
19548
- type: "button",
19549
- ...unlinkButtonProps
19550
- },
19551
- /* @__PURE__ */ React.createElement(Unlink, { width: 18 })
19552
- ));
19553
- return /* @__PURE__ */ React.createElement(PortalBody, null, /* @__PURE__ */ React.createElement(
19554
- "div",
19555
- {
19556
- ref: insertRef,
19557
- className: popoverVariants(),
19558
- ...insertProps,
19559
- style: {
19560
- ...insertProps.style,
19561
- zIndex: 999999
19562
- }
19563
- },
19564
- input
19565
- ), /* @__PURE__ */ React.createElement(
19566
- "div",
19567
- {
19568
- ref: editRef,
19569
- className: popoverVariants(),
19570
- ...editProps,
19571
- style: { ...editProps.style, zIndex: 999999 }
19572
- },
19573
- editContent
19574
- ));
19575
- }
19576
- function LinkOpenButton() {
19577
- const editor = useEditorRef();
19578
- const selection = useEditorSelection();
19579
- const attributes = React.useMemo(
19580
- () => {
19581
- const entry = editor.api.node({
19582
- match: { type: editor.getType(LinkPlugin) }
19583
- });
19584
- if (!entry) {
19585
- return {};
19586
- }
19587
- const [element] = entry;
19588
- return getLinkAttributes(editor, element);
19589
- },
19590
- // eslint-disable-next-line react-hooks/exhaustive-deps
19591
- [editor, selection]
19592
- );
19593
- return /* @__PURE__ */ React.createElement(
19594
- "a",
19595
- {
19596
- ...attributes,
19597
- className: buttonVariants$2({
19598
- size: "icon",
19599
- variant: "ghost"
19600
- }),
19601
- onMouseOver: (e) => {
19602
- e.stopPropagation();
19603
- },
19604
- "aria-label": "Open link in a new tab",
19605
- target: "_blank"
19606
- },
19607
- /* @__PURE__ */ React.createElement(ExternalLink, { width: 18 })
19608
- );
19609
- }
19610
- const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
19611
- const emailLintRE = /mailto:([^?\\]+)/;
19612
- const telLintRE = /tel:([\d-]+)/;
19613
- const localhostDomainRE = /^localhost[\d:?]*(?:[^\d:?]\S*)?$/;
19614
- const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/;
19615
- const localUrlRE = /^\/\S+/;
19616
- const isUrl = (string) => {
19617
- if (typeof string !== "string") {
19618
- return false;
19619
- }
19620
- if (string.startsWith("#")) {
19621
- return true;
19622
- }
19623
- const generalMatch = string.match(protocolAndDomainRE);
19624
- const emailLinkMatch = string.match(emailLintRE);
19625
- const telLinkMatch = string.match(telLintRE);
19626
- const localUrlMatch = string.match(localUrlRE);
19627
- if (emailLinkMatch || telLinkMatch || localUrlMatch) {
19628
- return true;
19629
- }
19630
- if (generalMatch) {
19631
- const everythingAfterProtocol = generalMatch[1];
19632
- if (!everythingAfterProtocol) {
19633
- return false;
19634
- }
19635
- try {
19636
- new URL(string);
19637
- } catch {
19638
- return false;
19639
- }
19640
- return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol);
19641
- }
19642
- return false;
19643
- };
19644
- const buildError = (element) => {
19645
- return {
19646
- message: element.message,
19647
- position: element.position && {
19648
- endColumn: element.position.end.column,
19649
- startColumn: element.position.start.column,
19650
- startLineNumber: element.position.start.line,
19651
- endLineNumber: element.position.end.line
19652
- }
19653
- };
19654
- };
19655
- const buildErrorMessage = (element) => {
19656
- if (!element) {
19657
- return "";
19658
- }
19659
- const errorMessage = buildError(element);
19660
- const message = errorMessage ? `${errorMessage.message}${errorMessage.position ? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}` : ""}` : null;
19661
- return message;
19662
- };
19663
- const ELEMENT_INVALID_MARKDOWN = "invalid_markdown";
19664
- const createInvalidMarkdownPlugin = createPlatePlugin({
19665
- key: ELEMENT_INVALID_MARKDOWN,
19666
- options: {
19667
- isElement: true,
19668
- isVoid: true,
19669
- isInline: false
19670
- },
19671
- node: {
19672
- component: InvalidMarkdownElement
19673
- }
19674
- });
19675
- function InvalidMarkdownElement({
19676
- attributes,
19677
- element,
19678
- children
19679
- }) {
19680
- return /* @__PURE__ */ React__default.createElement("div", { ...attributes }, /* @__PURE__ */ React__default.createElement(ErrorMessage, { error: element }), children);
19681
- }
19682
- function ErrorMessage({ error }) {
19683
- const message = buildErrorMessage(error);
19684
- const { setRawMode } = useEditorContext();
19685
- 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(
19686
- "button",
19687
- {
19688
- type: "button",
19689
- onClick: () => setRawMode(true),
19690
- 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"
19691
- },
19692
- "Switch to raw-mode"
19693
- ))));
19694
- }
19695
- const useCustomFloatingToolbarState = ({
19696
- editorId,
19697
- floatingOptions,
19698
- focusedEditorId,
19699
- hideToolbar,
19700
- showWhenReadOnly
19701
- }) => {
19702
- const editor = useEditorRef();
19703
- const selectionExpanded = useEditorSelector(
19704
- () => editor.api.isExpanded(),
19705
- []
19706
- );
19707
- const selectionText = useEditorSelector(() => editor.api.string(), []);
19708
- const readOnly = useEditorReadOnly();
19709
- const focused = useFocused();
19710
- const [open, setOpen] = React__default.useState(false);
19711
- const [waitForCollapsedSelection, setWaitForCollapsedSelection] = React__default.useState(false);
19712
- const [mousedown, setMousedown] = React__default.useState(false);
19713
- const floating = useVirtualFloating(
19714
- mergeProps(
19715
- {
19716
- open,
19717
- getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
19718
- onOpenChange: setOpen
19719
- },
19720
- floatingOptions
19721
- )
19722
- );
19723
- return {
19724
- editorId,
19725
- floating,
19726
- focused,
19727
- focusedEditorId,
19728
- hideToolbar,
19729
- mousedown,
19730
- open,
19731
- readOnly,
19732
- selectionExpanded,
19733
- selectionText,
19734
- setMousedown,
19735
- setOpen,
19736
- setWaitForCollapsedSelection,
19737
- showWhenReadOnly,
19738
- waitForCollapsedSelection
19739
- };
19740
- };
19741
- const useCustomFloatingToolbar = ({
19742
- editorId,
19743
- floating,
19744
- focusedEditorId,
19745
- hideToolbar,
19746
- mousedown,
19747
- open,
19748
- readOnly,
19749
- selectionExpanded,
19750
- selectionText,
19751
- setMousedown,
19752
- setOpen,
19753
- setWaitForCollapsedSelection,
19754
- showWhenReadOnly,
19755
- waitForCollapsedSelection
19756
- }) => {
19757
- React__default.useEffect(() => {
19758
- if (!(editorId === focusedEditorId)) {
19759
- setWaitForCollapsedSelection(true);
19760
- }
19761
- if (!selectionExpanded) {
19762
- setWaitForCollapsedSelection(false);
19763
- }
19764
- }, [
19765
- editorId,
19766
- focusedEditorId,
19767
- selectionExpanded,
19768
- setWaitForCollapsedSelection
19769
- ]);
19770
- React__default.useEffect(() => {
19771
- const mouseup = () => setMousedown(false);
19772
- const mousedown2 = () => setMousedown(true);
19773
- document.addEventListener("mouseup", mouseup);
19774
- document.addEventListener("mousedown", mousedown2);
19775
- return () => {
19776
- document.removeEventListener("mouseup", mouseup);
19777
- document.removeEventListener("mousedown", mousedown2);
19778
- };
19779
- }, []);
19780
- React__default.useEffect(() => {
19781
- if (!selectionExpanded || !selectionText || mousedown && !open || hideToolbar || readOnly && !showWhenReadOnly) {
19782
- setOpen(false);
19783
- } else if (selectionText && selectionExpanded && (!waitForCollapsedSelection || readOnly)) {
19784
- setOpen(true);
19785
- }
19786
- }, [
19787
- setOpen,
19788
- editorId,
19789
- focusedEditorId,
19790
- hideToolbar,
19791
- showWhenReadOnly,
19792
- selectionExpanded,
19793
- selectionText,
19794
- mousedown,
19795
- waitForCollapsedSelection,
19796
- open,
19797
- readOnly
19798
- ]);
19799
- const { update } = floating;
19800
- useEditorSelector(() => {
19801
- update == null ? void 0 : update();
19802
- }, [update]);
19803
- const clickOutsideRef = useOnClickOutside(
19804
- () => {
19805
- setOpen(false);
19806
- },
19807
- {
19808
- ignoreClass: "ignore-click-outside/toolbar"
19809
- }
19810
- );
19811
- return {
19812
- clickOutsideRef,
19813
- hidden: !open,
19814
- props: {
19815
- style: floating.style
19816
- },
19817
- ref: floating.refs.setFloating
19818
- };
19819
- };
19820
- const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
19821
- const editorId = useEditorRef();
19822
- const focusedEditorId = useEventEditorValue("focus");
19823
- const test = useCustomFloatingToolbarState({
19824
- editorId: editorId.id,
19825
- focusedEditorId,
19826
- ...state,
19827
- floatingOptions: {
19828
- middleware: [
19829
- offset(12),
19830
- flip({
19831
- fallbackPlacements: [
19832
- "top-start",
19833
- "top-end",
19834
- "bottom-start",
19835
- "bottom-end"
19836
- ],
19837
- padding: 12
19838
- })
19839
- ],
19840
- placement: "top",
19841
- ...state == null ? void 0 : state.floatingOptions
19842
- }
19843
- });
19844
- const {
19845
- hidden,
19846
- props: rootProps,
19847
- ref: floatingRef
19848
- } = useCustomFloatingToolbar(test);
19849
- const ref = useComposedRef$1(propRef, floatingRef);
19850
- if (hidden) return null;
19851
- return /* @__PURE__ */ React__default.createElement(PortalBody, null, /* @__PURE__ */ React__default.createElement(
19852
- Toolbar,
19853
- {
19854
- className: cn$1(
19855
- "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
19856
- ),
19857
- ...props,
19858
- ...rootProps,
19859
- ref
19860
- },
19861
- children
19862
- ));
19863
- });
19864
- function TurnIntoDropdownMenu(props) {
19865
- const { headingLevels } = useToolbarContext();
19866
- const items = [
19867
- paragraphItem,
19868
- ...headingLevels.map((level) => headingItemsByLevel[level])
19869
- ];
19870
- const value = useEditorSelector((editor2) => {
19871
- let initialNodeType = ParagraphPlugin.key;
19872
- let allNodesMatchInitialNodeType = false;
19873
- const codeBlockEntries = editor2.api.nodes({
19874
- match: (n) => editor2.api.isBlock(n),
19875
- mode: "highest"
19876
- });
19877
- const nodes = Array.from(codeBlockEntries);
19878
- if (nodes.length > 0) {
19879
- initialNodeType = nodes[0][0].type;
19880
- allNodesMatchInitialNodeType = nodes.every(([node]) => {
19881
- const type2 = (node == null ? void 0 : node.type) || ParagraphPlugin.key;
19882
- return type2 === initialNodeType;
19883
- });
19884
- }
19885
- return allNodesMatchInitialNodeType ? initialNodeType : ParagraphPlugin.key;
19886
- }, []);
19887
- const editor = useEditorRef();
19888
- const openState = useOpenState();
19889
- const selectedItem = getHeadingItem(value) ?? paragraphItem;
19890
- const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
19891
- const editorState = useEditorState();
19892
- const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
19893
- if (userInTable) return null;
19894
- return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
19895
- ToolbarButton,
19896
- {
19897
- className: "lg:min-w-[130px]",
19898
- isDropdown: true,
19899
- showArrow: true,
19900
- pressed: openState.open,
19901
- tooltip: "Turn into"
19902
- },
19903
- /* @__PURE__ */ React__default.createElement("span", { className: "" }, selectedItemLabel)
19904
- )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-0" }, /* @__PURE__ */ React__default.createElement(DropdownMenuLabel, null, "Turn into"), /* @__PURE__ */ React__default.createElement(
19905
- DropdownMenuRadioGroup,
19906
- {
19907
- className: "flex flex-col gap-0.5",
19908
- onValueChange: (type2) => {
19909
- if (type2 === "ul" || type2 === "ol") {
19910
- toggleList(editor, { type: type2 });
19911
- } else {
19912
- unwrapList(editor);
19913
- editor.tf.toggleBlock(type2);
19914
- }
19915
- editor.tf.collapse();
19916
- editor.tf.focus();
19917
- },
19918
- value
19919
- },
19920
- items.map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
19921
- DropdownMenuRadioItem,
19922
- {
19923
- className: "min-w-[180px]",
19924
- key: itemValue,
19925
- value: itemValue
19926
- },
19927
- /* @__PURE__ */ React__default.createElement(Icon, { className: "mr-2 size-5" }),
19928
- label
19929
- ))
19930
- )));
19931
- }
19932
- function FloatingToolbarButtons() {
19933
- return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(TurnIntoDropdownMenu, null));
19934
- }
19935
- const FloatingToolbarPlugin = createPlatePlugin({
19936
- key: "floating-toolbar",
19937
- render: {
19938
- afterEditable: () => /* @__PURE__ */ React__default.createElement(FloatingToolbar, null, /* @__PURE__ */ React__default.createElement(FloatingToolbarButtons, null))
19939
- }
19940
- });
19941
19984
  function _1c(hljs) {
19942
19985
  const UNDERSCORE_IDENT_RE = "[A-Za-zА-Яа-яёЁ_][A-Za-zА-Яа-яёЁ_0-9]+";
19943
19986
  const v7_keywords = "далее ";
@@ -70583,6 +70626,252 @@ class HastEmitter {
70583
70626
  return "";
70584
70627
  }
70585
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
+ };
70586
70875
  const createHTMLBlockPlugin = createPlatePlugin({
70587
70876
  key: "html",
70588
70877
  node: {
@@ -70599,18 +70888,26 @@ const createHTMLInlinePlugin = createPlatePlugin({
70599
70888
  isInline: true
70600
70889
  }
70601
70890
  });
70602
- const KEY_BLOCKQUOTE_ENTER_BREAK = "blockquote-enter-break";
70603
- const createBlockquoteEnterBreakPlugin = createPlatePlugin({
70604
- 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,
70605
70899
  handlers: {
70606
70900
  onKeyDown: ({ editor, event }) => {
70607
70901
  var _a;
70608
- if (event.key !== "Enter") return;
70609
- const blockquoteEntry = editor.api.above({
70902
+ if (event.key !== "Enter" || event.metaKey || event.ctrlKey) return;
70903
+ const inBlockquote = editor.api.above({
70610
70904
  match: { type: BlockquotePlugin.key }
70611
70905
  });
70612
- 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;
70613
70909
  event.preventDefault();
70910
+ if (editor.api.isExpanded()) editor.tf.delete();
70614
70911
  const cursorPosition = (_a = editor.selection) == null ? void 0 : _a.focus;
70615
70912
  if (!cursorPosition) return;
70616
70913
  editor.tf.insertNodes(
@@ -70638,6 +70935,303 @@ const createBreakPlugin = createPlatePlugin({
70638
70935
  }
70639
70936
  }
70640
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
+ });
70641
71235
  [
70642
71236
  HEADING_KEYS.h1,
70643
71237
  HEADING_KEYS.h2,
@@ -70702,7 +71296,7 @@ const createEditorPlugins = ({
70702
71296
  createImgPlugin,
70703
71297
  createHTMLBlockPlugin,
70704
71298
  createHTMLInlinePlugin,
70705
- createBlockquoteEnterBreakPlugin,
71299
+ createHardBreakPlugin,
70706
71300
  createInvalidMarkdownPlugin,
70707
71301
  CorrectNodeBehaviorPlugin,
70708
71302
  ClearHighlightOnEnterPlugin,
@@ -70801,6 +71395,8 @@ const createEditorPlugins = ({
70801
71395
  ]
70802
71396
  }
70803
71397
  }),
71398
+ // Only reached where `createHardBreakPlugin` bails (code blocks, table
71399
+ // cells) — everywhere else it has already inserted a `break` element.
70804
71400
  SoftBreakPlugin.configure({
70805
71401
  options: {
70806
71402
  rules: [
@@ -70981,6 +71577,7 @@ const MediaWorkflowOverlay = () => {
70981
71577
  phase: "confirming",
70982
71578
  branchName: event.branchName,
70983
71579
  baseBranch: event.baseBranch,
71580
+ allowSaveToProtectedBranch: event.allowSaveToProtectedBranch,
70984
71581
  onConfirm: event.onConfirm,
70985
71582
  onCancel: event.onCancel,
70986
71583
  onSaveToProtectedBranch: event.onSaveToProtectedBranch
@@ -71033,7 +71630,7 @@ const MediaWorkflowOverlay = () => {
71033
71630
  if (state.phase !== "confirming") return;
71034
71631
  const confirmState = state;
71035
71632
  const branchName = confirmState.branchName;
71036
- const targetBranch = `tina/${branchName}`;
71633
+ const targetBranch = `tina/${normalizeBranchName(branchName)}`;
71037
71634
  abortPreflight();
71038
71635
  const abortController = new AbortController();
71039
71636
  preflightAbortRef.current = abortController;
@@ -71101,7 +71698,7 @@ const MediaWorkflowOverlay = () => {
71101
71698
  state.onCancel();
71102
71699
  setState({ phase: "idle" });
71103
71700
  },
71104
- disabled: state.branchName === "" || state.isChecking,
71701
+ disabled: normalizeBranchName(state.branchName) === "" || state.isChecking,
71105
71702
  errorMessage: state.errorMessage,
71106
71703
  onBranchNameChange: (branchName) => {
71107
71704
  abortPreflight();
@@ -71115,6 +71712,7 @@ const MediaWorkflowOverlay = () => {
71115
71712
  );
71116
71713
  },
71117
71714
  onCreateBranch: handleCreateBranch,
71715
+ allowSaveToProtectedBranch: state.allowSaveToProtectedBranch,
71118
71716
  onSaveToProtectedBranch: () => {
71119
71717
  abortPreflight();
71120
71718
  state.onSaveToProtectedBranch();
@@ -71304,6 +71902,14 @@ const DefaultSessionProvider = ({
71304
71902
  children
71305
71903
  }) => /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, children);
71306
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
+ }
71307
71913
  /**
71308
71914
  * Wraps the normal fetch function with same API but adds the authorization header token.
71309
71915
  *
@@ -71314,16 +71920,24 @@ class AbstractAuthProvider {
71314
71920
  * @param init fetch function init
71315
71921
  */
71316
71922
  async fetchWithToken(input, init) {
71923
+ var _a;
71317
71924
  const headers = (init == null ? void 0 : init.headers) || {};
71318
- const token = await this.getToken();
71319
- const accessToken = (token == null ? void 0 : token.access_token) ?? (token == null ? void 0 : token.id_token);
71925
+ const accessToken = await this.getAccessToken();
71320
71926
  if (accessToken) {
71321
71927
  headers["Authorization"] = "Bearer " + accessToken;
71322
71928
  }
71323
- return await fetch(input, {
71929
+ const res = await fetch(input, {
71324
71930
  ...init || {},
71325
71931
  headers: new Headers(headers)
71326
71932
  });
71933
+ if (res.status === 401 && accessToken) {
71934
+ (_a = this.sessionExpiredListener) == null ? void 0 : _a.call(this);
71935
+ }
71936
+ return res;
71937
+ }
71938
+ async getAccessToken() {
71939
+ const token = await this.getToken();
71940
+ return (token == null ? void 0 : token.access_token) ?? (token == null ? void 0 : token.id_token) ?? null;
71327
71941
  }
71328
71942
  async authorize(context) {
71329
71943
  return this.getToken();
@@ -71362,6 +71976,7 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71362
71976
  __publicField(this, "frontendUrl");
71363
71977
  __publicField(this, "token");
71364
71978
  // used with memory storage
71979
+ __publicField(this, "hasWarnedNoSession", false);
71365
71980
  __publicField(this, "setToken");
71366
71981
  __publicField(this, "getToken");
71367
71982
  this.frontendUrl = frontendUrl;
@@ -71430,16 +72045,36 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71430
72045
  }
71431
72046
  const url = `${this.identityApiUrl}/v2/apps/${this.clientId}/currentUser`;
71432
72047
  try {
71433
- const res = await this.fetchWithToken(url, {
71434
- method: "GET"
71435
- });
72048
+ if (!await this.getAccessToken()) {
72049
+ if (!this.hasWarnedNoSession) {
72050
+ this.hasWarnedNoSession = true;
72051
+ console.warn(
72052
+ "TinaCMS: no TinaCloud session found. If login fails, check the console inside the login popup window for the underlying error."
72053
+ );
72054
+ }
72055
+ return null;
72056
+ }
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
+ }
71436
72065
  const val = await res.json();
71437
72066
  if (!res.status.toString().startsWith("2")) {
71438
- console.error(val.error);
72067
+ console.error(
72068
+ `TinaCMS: TinaCloud session check failed (status ${res.status}).`,
72069
+ (val == null ? void 0 : val.error) ?? val
72070
+ );
71439
72071
  return null;
71440
72072
  }
71441
72073
  return val;
71442
72074
  } catch (e) {
72075
+ if (e instanceof TypeError || isErrorNamed(e, "AbortError")) {
72076
+ throw e;
72077
+ }
71443
72078
  console.error(e);
71444
72079
  return null;
71445
72080
  }
@@ -71556,6 +72191,8 @@ class Client {
71556
72191
  // automatically hooked into global event bus when attached via cms.
71557
72192
  __publicField(this, "protectedBranches", []);
71558
72193
  __publicField(this, "usingEditorialWorkflow", false);
72194
+ // Whatever the auth provider returned from getUser().
72195
+ __publicField(this, "user");
71559
72196
  __publicField(this, "addPendingContent", async (props) => {
71560
72197
  const mutation = `#graphql
71561
72198
  mutation addPendingDocumentMutation(
@@ -71655,13 +72292,24 @@ mutation addPendingDocumentMutation(
71655
72292
  }
71656
72293
  );
71657
72294
  this.clientId = options.clientId;
71658
- this.authProvider = ((_u = (_t = (_s = this.schema) == null ? void 0 : _s.config) == null ? void 0 : _t.config) == null ? void 0 : _u.authProvider) || new TinaCloudAuthProvider({
71659
- clientId: options.clientId,
71660
- identityApiUrl: this.identityApiUrl,
71661
- getTokenFn: options.getTokenFn,
71662
- tokenStorage,
71663
- frontendUrl: this.frontendUrl
71664
- });
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;
71665
72313
  }
71666
72314
  get isLocalMode() {
71667
72315
  return false;
@@ -71706,6 +72354,17 @@ mutation addPendingDocumentMutation(
71706
72354
  variables
71707
72355
  })
71708
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
+ }
71709
72368
  if (res.status !== 200) {
71710
72369
  let errorMessage = `Unable to complete request, ${res.statusText}`;
71711
72370
  const resBody = await res.json();
@@ -72127,6 +72786,10 @@ mutation addPendingDocumentMutation(
72127
72786
  throw error;
72128
72787
  }
72129
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
+ */
72130
72793
  async startMediaEditorialWorkflow(options) {
72131
72794
  const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}/media`;
72132
72795
  return await this.postEditorialWorkflow(
@@ -72151,7 +72814,9 @@ class LocalClient extends Client {
72151
72814
  customContentApiUrl: props && props.customContentApiUrl ? props.customContentApiUrl : DEFAULT_LOCAL_TINA_GQL_SERVER_URL
72152
72815
  };
72153
72816
  super(clientProps);
72154
- 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
+ );
72155
72820
  }
72156
72821
  get isLocalMode() {
72157
72822
  return true;
@@ -72396,6 +73061,7 @@ const useTinaAuthRedirect = (params, enabled = true) => {
72396
73061
  function sleep(ms) {
72397
73062
  return new Promise((resolve) => setTimeout(resolve, ms));
72398
73063
  }
73064
+ const SESSION_EXPIRED_MESSAGE = "Your session has ended. Please sign in again to continue editing.";
72399
73065
  const AuthWallInner = ({
72400
73066
  children,
72401
73067
  cms,
@@ -72417,6 +73083,15 @@ const AuthWallInner = ({
72417
73083
  const [showChildren, setShowChildren] = useState(false);
72418
73084
  const [authProps, setAuthProps] = useState({ username: "", password: "" });
72419
73085
  const [authenticated, setAuthenticated] = useState(false);
73086
+ const [sessionExpired, setSessionExpired] = useState(false);
73087
+ React__default.useEffect(
73088
+ () => cms.events.subscribe("cms:session-expired", () => {
73089
+ setSessionExpired(true);
73090
+ setShowChildren(false);
73091
+ setActiveModal("authenticate");
73092
+ }),
73093
+ []
73094
+ );
72420
73095
  React__default.useEffect(() => {
72421
73096
  let mounted = true;
72422
73097
  if (isAuthRedirect) {
@@ -72432,6 +73107,7 @@ const AuthWallInner = ({
72432
73107
  if (!mounted) return;
72433
73108
  if (isAuthorized) {
72434
73109
  const user = await client.authProvider.getUser();
73110
+ client.user = user;
72435
73111
  if (user.passwordChangeRequired) {
72436
73112
  window.location.hash = "#/screens/change_password";
72437
73113
  }
@@ -72467,6 +73143,7 @@ const AuthWallInner = ({
72467
73143
  }, [authenticated]);
72468
73144
  const onAuthenticated = async () => {
72469
73145
  setAuthenticated(true);
73146
+ setSessionExpired(false);
72470
73147
  setActiveModal(null);
72471
73148
  cms.events.dispatch({ type: "cms:login" });
72472
73149
  };
@@ -72486,7 +73163,7 @@ const AuthWallInner = ({
72486
73163
  }
72487
73164
  return onAuthenticated();
72488
73165
  } catch (e) {
72489
- if (e instanceof AuthenticationCancelledError || (e == null ? void 0 : e.name) === "AuthenticationCancelledError") {
73166
+ if (e instanceof AuthenticationCancelledError || isErrorNamed(e, "AuthenticationCancelledError")) {
72490
73167
  return;
72491
73168
  }
72492
73169
  console.error(e);
@@ -72513,6 +73190,7 @@ const AuthWallInner = ({
72513
73190
  ModalBuilder,
72514
73191
  {
72515
73192
  title: modalTitle,
73193
+ error: sessionExpired ? SESSION_EXPIRED_MESSAGE : void 0,
72516
73194
  message: isTinaCloud ? /* @__PURE__ */ React__default.createElement(
72517
73195
  "img",
72518
73196
  {
@@ -72538,6 +73216,7 @@ const AuthWallInner = ({
72538
73216
  ModalBuilder,
72539
73217
  {
72540
73218
  title: modalTitle,
73219
+ error: sessionExpired ? SESSION_EXPIRED_MESSAGE : void 0,
72541
73220
  message: "",
72542
73221
  close,
72543
73222
  busy: isAuthRedirect,
@@ -72759,9 +73438,14 @@ const TinaCloudProvider = (props) => {
72759
73438
  }
72760
73439
  }, []);
72761
73440
  React__default.useEffect(() => {
72762
- const setupEditorialWorkflow = () => {
72763
- client.getProject().then(async (project) => {
72764
- var _a2;
73441
+ const setupEditorialWorkflow = async () => {
73442
+ var _a2;
73443
+ try {
73444
+ const token = await client.authProvider.getToken();
73445
+ if (!(token == null ? void 0 : token.access_token) && !(token == null ? void 0 : token.id_token)) {
73446
+ return;
73447
+ }
73448
+ const project = await client.getProject();
72765
73449
  if ((_a2 = project == null ? void 0 : project.features) == null ? void 0 : _a2.includes("editorial-workflow")) {
72766
73450
  cms.flags.set("branch-switcher", true);
72767
73451
  client.usingEditorialWorkflow = true;
@@ -72770,7 +73454,9 @@ const TinaCloudProvider = (props) => {
72770
73454
  setCurrentBranch(project.defaultBranch || "main");
72771
73455
  }
72772
73456
  }
72773
- });
73457
+ } catch (e) {
73458
+ console.error("TinaCMS: unable to load project settings", e);
73459
+ }
72774
73460
  };
72775
73461
  if (isTinaCloud) {
72776
73462
  setupEditorialWorkflow();
@@ -73826,6 +74512,7 @@ const CollectionListPage = () => {
73826
74512
  );
73827
74513
  return (_b2 = (_a2 = doc == null ? void 0 : doc.document) == null ? void 0 : _a2._sys) == null ? void 0 : _b2.hasReferences;
73828
74514
  } catch (error) {
74515
+ if (isSessionExpiredError(error)) throw error;
73829
74516
  cms.alerts.error(
73830
74517
  "Document was not found, ask a developer for help or check the console for an error message"
73831
74518
  );
@@ -73841,6 +74528,7 @@ const CollectionListPage = () => {
73841
74528
  );
73842
74529
  reFetchCollection();
73843
74530
  } catch (error) {
74531
+ if (isSessionExpiredError(error)) throw error;
73844
74532
  if (error.message.includes(ERR_HAS_REFERENCES)) {
73845
74533
  cms.alerts.error(
73846
74534
  error.message.split("\n ").filter(Boolean)[1]
@@ -73870,6 +74558,7 @@ const CollectionListPage = () => {
73870
74558
  );
73871
74559
  reFetchCollection();
73872
74560
  } catch (error) {
74561
+ if (isSessionExpiredError(error)) throw error;
73873
74562
  cms.alerts.warn(
73874
74563
  "Document was not deleted, ask a developer for help or check the console for an error message"
73875
74564
  );
@@ -73902,6 +74591,7 @@ const CollectionListPage = () => {
73902
74591
  );
73903
74592
  reFetchCollection();
73904
74593
  } catch (error) {
74594
+ if (isSessionExpiredError(error)) throw error;
73905
74595
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
73906
74596
  cms.alerts.error(
73907
74597
  `Document was not renamed. The filename "${vars.newRelativePath}" is already used by another document, please choose a different name.`
@@ -73948,6 +74638,7 @@ const CollectionListPage = () => {
73948
74638
  );
73949
74639
  cms.alerts.info("Folder was successfully created");
73950
74640
  } catch (error) {
74641
+ if (isSessionExpiredError(error)) throw error;
73951
74642
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
73952
74643
  cms.alerts.error(
73953
74644
  `Folder was not created, folder with name "${vars.folderName}" already exists ${folder.name ? `in ${folder.name}` : ""}`
@@ -74511,6 +75202,7 @@ const DeleteModal = ({
74511
75202
  useEffect(() => {
74512
75203
  checkRefsFunc().then((result) => {
74513
75204
  setHasRefs(result);
75205
+ }).catch(() => {
74514
75206
  });
74515
75207
  }, [filename, checkRefsFunc]);
74516
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(
@@ -74519,7 +75211,10 @@ const DeleteModal = ({
74519
75211
  style: { flexGrow: 3 },
74520
75212
  variant: "danger",
74521
75213
  onClick: async () => {
74522
- await deleteFunc();
75214
+ try {
75215
+ await deleteFunc();
75216
+ } catch {
75217
+ }
74523
75218
  close2();
74524
75219
  }
74525
75220
  },
@@ -74657,6 +75352,18 @@ const shouldAutoOpenCollectionDocument = ({
74657
75352
  ((_b = (_a = collection.documents) == null ? void 0 : _a.edges) == null ? void 0 : _b.length) === 1 && // Check to make sure the file is not a folder
74658
75353
  ((_e = (_d = (_c = collection.documents) == null ? void 0 : _c.edges[0]) == null ? void 0 : _d.node) == null ? void 0 : _e.__typename) !== "Folder";
74659
75354
  };
75355
+ const UnableToLoadModal = ({ message }) => /* @__PURE__ */ React__default.createElement(Modal, null, /* @__PURE__ */ React__default.createElement(PopupModal, null, /* @__PURE__ */ React__default.createElement(ModalHeader, null, "Unable to load"), /* @__PURE__ */ React__default.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React__default.createElement("div", { className: "tina-prose" }, message)), /* @__PURE__ */ React__default.createElement(ModalActions, null, /* @__PURE__ */ React__default.createElement("div", { className: "flex-1" }), /* @__PURE__ */ React__default.createElement(
75356
+ Button$2,
75357
+ {
75358
+ style: { flexGrow: 1 },
75359
+ className: "w-full",
75360
+ variant: "primary",
75361
+ onClick: () => {
75362
+ window.location.reload();
75363
+ }
75364
+ },
75365
+ "Reload"
75366
+ ))));
74660
75367
  const isValidSortKey = (sortKey, collection) => {
74661
75368
  if (collection.fields) {
74662
75369
  const sortKeys = collection.fields.map((x) => x.name);
@@ -74697,10 +75404,13 @@ const useGetCollection = (cms, collectionName, includeDocuments = true, folder,
74697
75404
  useEffect(() => {
74698
75405
  let cancelled = false;
74699
75406
  const fetchCollection = async () => {
74700
- if (await api.isAuthenticated() && !folder.loading && !cancelled) {
74701
- const { name: name2, order } = JSON.parse(sortKey || "{}");
74702
- const validSortKey = isValidSortKey(name2, collectionExtra) ? name2 : void 0;
74703
- try {
75407
+ if (folder.loading) {
75408
+ return;
75409
+ }
75410
+ try {
75411
+ if (await api.isAuthenticated() && !cancelled) {
75412
+ const { name: name2, order } = JSON.parse(sortKey || "{}");
75413
+ const validSortKey = isValidSortKey(name2, collectionExtra) ? name2 : void 0;
74704
75414
  const collection2 = await api.fetchCollection(
74705
75415
  collectionName,
74706
75416
  includeDocuments,
@@ -74711,14 +75421,21 @@ const useGetCollection = (cms, collectionName, includeDocuments = true, folder,
74711
75421
  filterArgs
74712
75422
  );
74713
75423
  setCollection(collection2);
74714
- } catch (error2) {
75424
+ } else if (!cancelled) {
75425
+ dispatchSessionExpired(cms.events);
75426
+ setCollection(void 0);
75427
+ }
75428
+ } catch (error2) {
75429
+ setCollection(void 0);
75430
+ if (!isSessionExpiredError(error2)) {
74715
75431
  cms.alerts.error(
74716
75432
  `[${error2.name}] GetCollection failed: ${error2.message}`
74717
75433
  );
74718
75434
  console.error(error2);
74719
- setCollection(void 0);
74720
75435
  setError(error2);
74721
75436
  }
75437
+ }
75438
+ if (!cancelled) {
74722
75439
  setLoading(false);
74723
75440
  }
74724
75441
  };
@@ -74751,8 +75468,11 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74751
75468
  useEffect(() => {
74752
75469
  let cancelled = false;
74753
75470
  const searchCollection = async () => {
74754
- if (await api.isAuthenticated() && !folder.loading && !cancelled) {
74755
- try {
75471
+ if (folder.loading) {
75472
+ return;
75473
+ }
75474
+ try {
75475
+ if (await api.isAuthenticated() && !cancelled) {
74756
75476
  const response = await cms.api.search.query(search, {
74757
75477
  limit: 15,
74758
75478
  cursor: after,
@@ -74765,6 +75485,15 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74765
75485
  return api.fetchDocument(collection2, relativePath, false);
74766
75486
  })
74767
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
+ }
74768
75497
  const edges = docs.filter((p) => {
74769
75498
  var _a;
74770
75499
  return p.status === "fulfilled" && !!((_a = p.value) == null ? void 0 : _a.document);
@@ -74786,14 +75515,21 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74786
75515
  }
74787
75516
  };
74788
75517
  setCollection(collectionData);
74789
- } catch (error2) {
75518
+ } else if (!cancelled) {
75519
+ dispatchSessionExpired(cms.events);
75520
+ setCollection(void 0);
75521
+ }
75522
+ } catch (error2) {
75523
+ setCollection(void 0);
75524
+ if (!isSessionExpiredError(error2)) {
74790
75525
  cms.alerts.error(
74791
75526
  `[${error2.name}] GetCollection failed: ${error2.message}`
74792
75527
  );
74793
75528
  console.error(error2);
74794
- setCollection(void 0);
74795
75529
  setError(error2);
74796
75530
  }
75531
+ }
75532
+ if (!cancelled) {
74797
75533
  setLoading(false);
74798
75534
  }
74799
75535
  };
@@ -74851,7 +75587,7 @@ const GetCollection = ({
74851
75587
  );
74852
75588
  useEffect(() => {
74853
75589
  var _a, _b, _c, _d;
74854
- if (loading) return;
75590
+ if (loading || !collection) return;
74855
75591
  const collectionDefinition = cms.api.tina.schema.getCollection(
74856
75592
  collection.name
74857
75593
  );
@@ -74880,6 +75616,9 @@ const GetCollection = ({
74880
75616
  if (loading) {
74881
75617
  return /* @__PURE__ */ React__default.createElement(LoadingPage, null);
74882
75618
  }
75619
+ if (!collection) {
75620
+ return /* @__PURE__ */ React__default.createElement(UnableToLoadModal, { message: "This collection could not be loaded." });
75621
+ }
74883
75622
  return /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, children(collection, loading, reFetchCollection, collectionExtra));
74884
75623
  };
74885
75624
  const createDocument = async (cms, collection, template, mutationInfo, folder, values) => {
@@ -74898,9 +75637,7 @@ const createDocument = async (cms, collection, template, mutationInfo, folder, v
74898
75637
  if (await api.isAuthenticated()) {
74899
75638
  await api.createDocument(collection, relativePath, params);
74900
75639
  } else {
74901
- const authMessage = `CreateDocument failed: User is no longer authenticated; please login and try again.`;
74902
- cms.alerts.error(authMessage);
74903
- console.error(authMessage);
75640
+ dispatchSessionExpired(cms.events);
74904
75641
  return false;
74905
75642
  }
74906
75643
  };
@@ -75083,7 +75820,7 @@ const RenderForm$1 = ({
75083
75820
  onSubmit: async (values) => {
75084
75821
  try {
75085
75822
  const folderName2 = folder.fullyQualifiedName ? folder.name : "";
75086
- await createDocument(
75823
+ const result = await createDocument(
75087
75824
  cms,
75088
75825
  collection,
75089
75826
  template,
@@ -75091,6 +75828,7 @@ const RenderForm$1 = ({
75091
75828
  folderName2,
75092
75829
  values
75093
75830
  );
75831
+ if (result === false) return;
75094
75832
  cms.alerts.success("Document created!");
75095
75833
  setTimeout(() => {
75096
75834
  navigate(
@@ -75098,6 +75836,7 @@ const RenderForm$1 = ({
75098
75836
  );
75099
75837
  }, 10);
75100
75838
  } catch (error) {
75839
+ if (isSessionExpiredError(error)) throw error;
75101
75840
  const defaultErrorText = "There was a problem saving your document.";
75102
75841
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
75103
75842
  cms.alerts.error(
@@ -75160,8 +75899,8 @@ const useGetDocument = (cms, collectionName, relativePath) => {
75160
75899
  useEffect(() => {
75161
75900
  let isCancelled = false;
75162
75901
  const fetchDocument = async () => {
75163
- if (api.isAuthenticated() && !isCancelled) {
75164
- try {
75902
+ try {
75903
+ if (await api.isAuthenticated() && !isCancelled) {
75165
75904
  const response = await api.fetchDocument(
75166
75905
  collectionName,
75167
75906
  relativePath
@@ -75169,19 +75908,24 @@ const useGetDocument = (cms, collectionName, relativePath) => {
75169
75908
  if (!isCancelled) {
75170
75909
  setDocument(response.document);
75171
75910
  }
75172
- } catch (error2) {
75173
- if (!isCancelled) {
75911
+ } else if (!isCancelled) {
75912
+ dispatchSessionExpired(cms.events);
75913
+ setDocument(void 0);
75914
+ }
75915
+ } catch (error2) {
75916
+ if (!isCancelled) {
75917
+ setDocument(void 0);
75918
+ if (!isSessionExpiredError(error2)) {
75174
75919
  cms.alerts.error(
75175
75920
  `[${error2.name}] GetDocument failed: ${error2.message}`
75176
75921
  );
75177
75922
  console.error(error2);
75178
- setDocument(void 0);
75179
75923
  setError(error2);
75180
75924
  }
75181
75925
  }
75182
- if (!isCancelled) {
75183
- setLoading(false);
75184
- }
75926
+ }
75927
+ if (!isCancelled) {
75928
+ setLoading(false);
75185
75929
  }
75186
75930
  };
75187
75931
  setLoading(true);
@@ -75209,6 +75953,9 @@ const GetDocument = ({
75209
75953
  if (loading) {
75210
75954
  return /* @__PURE__ */ React__default.createElement(LoadingPage, null);
75211
75955
  }
75956
+ if (!document2) {
75957
+ return /* @__PURE__ */ React__default.createElement(UnableToLoadModal, { message: "This document could not be loaded." });
75958
+ }
75212
75959
  return /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, children(document2, loading));
75213
75960
  };
75214
75961
  const CollectionDuplicatePage = () => {
@@ -75260,9 +76007,7 @@ const updateDocument = async (cms, relativePath, collection, mutationInfo, value
75260
76007
  if (await api.isAuthenticated()) {
75261
76008
  await api.updateDocument(collection, relativePath, params);
75262
76009
  } else {
75263
- const authMessage = `UpdateDocument failed: User is no longer authenticated; please login and try again.`;
75264
- cms.alerts.error(authMessage);
75265
- console.error(authMessage);
76010
+ dispatchSessionExpired(cms.events);
75266
76011
  return false;
75267
76012
  }
75268
76013
  };
@@ -75340,15 +76085,17 @@ const RenderForm = ({
75340
76085
  initialValues: document2._values,
75341
76086
  onSubmit: async (values) => {
75342
76087
  try {
75343
- await updateDocument(
76088
+ const result = await updateDocument(
75344
76089
  cms,
75345
76090
  relativePath,
75346
76091
  collection,
75347
76092
  mutationInfo,
75348
76093
  values
75349
76094
  );
76095
+ if (result === false) return;
75350
76096
  cms.alerts.success("Document updated!");
75351
76097
  } catch (error) {
76098
+ if (isSessionExpiredError(error)) throw error;
75352
76099
  cms.alerts.error(
75353
76100
  () => ErrorDialog({
75354
76101
  title: "There was a problem saving your document",
@@ -75558,6 +76305,9 @@ const CheckSchema = ({
75558
76305
  );
75559
76306
  }
75560
76307
  }).catch((error) => {
76308
+ if (isSessionExpiredError(error)) {
76309
+ return;
76310
+ }
75561
76311
  if (error.message.includes(ERR_NOT_INDEXED)) {
75562
76312
  setSchemaMissingError(true);
75563
76313
  } else {
@@ -75871,6 +76621,7 @@ export {
75871
76621
  BlocksFieldPlugin,
75872
76622
  BoldIcon$1 as BoldIcon,
75873
76623
  BranchButton,
76624
+ BranchCreator,
75874
76625
  BranchDataProvider,
75875
76626
  BranchItem,
75876
76627
  BranchPreviewButton,
@@ -76021,6 +76772,7 @@ export {
76021
76772
  Select$1 as Select,
76022
76773
  SelectField,
76023
76774
  SelectFieldPlugin,
76775
+ SessionExpiredError,
76024
76776
  SettingsIcon,
76025
76777
  StrikethroughIcon,
76026
76778
  StyledFile,
@@ -76073,6 +76825,7 @@ export {
76073
76825
  get,
76074
76826
  getFilteredBranchList,
76075
76827
  getStaticPropsForTina,
76828
+ isSessionExpiredError,
76076
76829
  passwordFieldClasses,
76077
76830
  resolveField,
76078
76831
  safeAssertShape,