tinacms 3.8.3 → 3.9.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 (26) hide show
  1. package/dist/index.d.ts +0 -4
  2. package/dist/index.js +1156 -567
  3. package/dist/internalClient/index.d.ts +30 -8
  4. package/dist/toolkit/components/media/index.d.ts +1 -1
  5. package/dist/toolkit/components/media/media-workflow-overlay.d.ts +2 -0
  6. package/dist/toolkit/components/media/utils.d.ts +8 -0
  7. package/dist/toolkit/core/event.d.ts +15 -1
  8. package/dist/toolkit/core/media-store.default.d.ts +31 -4
  9. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/components/plate-ui/command.d.ts +8 -8
  10. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/components/plate-ui/heading-items.d.ts +15 -0
  11. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/components/plate-ui/input.d.ts +1 -1
  12. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/plugins/core/autoformat/autoformat-block.d.ts +2 -0
  13. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/plugins/editor-plugins.d.ts +171 -165
  14. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/toolbar/toolbar-overrides.d.ts +3 -2
  15. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/toolbar/toolbar-provider.d.ts +10 -1
  16. package/dist/toolkit/form-builder/create-branch-modal.d.ts +9 -0
  17. package/dist/toolkit/form-builder/editorial-workflow-progress-modal.d.ts +15 -0
  18. package/dist/toolkit/form-builder/editorial-workflow-utils.d.ts +20 -0
  19. package/dist/toolkit/form-builder/use-editorial-workflow.d.ts +2 -0
  20. package/dist/toolkit/git-client/git-client.d.ts +0 -6
  21. package/dist/toolkit/git-client/git-file.d.ts +1 -1
  22. package/dist/toolkit/index.d.ts +2 -1
  23. package/dist/toolkit/plugin-branch-switcher/branch-switcher.d.ts +2 -1
  24. package/dist/toolkit/plugin-branch-switcher/format-branch-name.d.ts +8 -0
  25. package/dist/toolkit/react-core/use-cms-event.d.ts +1 -1
  26. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ import { useHTMLInputCursorState, useComboboxInput } from "@udecode/plate-combob
36
36
  import { useComboboxContext, Combobox as Combobox$1, useComboboxStore, ComboboxProvider, Portal, ComboboxPopover, ComboboxItem } from "@ariakit/react";
37
37
  import { filterWords } from "@udecode/plate-combobox";
38
38
  import { toggleList, unwrapList, getListItemEntry } from "@udecode/plate-list";
39
+ import { normalizeHeadingLevels, ALL_HEADING_LEVELS, isHeadingLevel, TinaSchema, addNamespaceToSchema, parseURL, resolveForm, normalizePath, canonicalPath, validateSchema } from "@tinacms/schema-tools";
40
+ import { NAMER, resolveField } from "@tinacms/schema-tools";
39
41
  import { useBlockSelected, BlockSelectionPlugin } from "@udecode/plate-selection/react";
40
42
  import "@udecode/plate-dnd";
41
43
  import { setCellBackground } from "@udecode/plate-table";
@@ -56,10 +58,10 @@ import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, close
56
58
  import { sortableKeyboardCoordinates, useSortable, SortableContext, verticalListSortingStrategy, defaultAnimateLayoutChanges } from "@dnd-kit/sortable";
57
59
  import { CSS } from "@dnd-kit/utilities";
58
60
  import { FaSpinner, FaCircle, FaFolder, FaFile, FaLock, FaUnlock } from "react-icons/fa";
61
+ import { AiFillWarning, AiOutlineLoading } from "react-icons/ai";
59
62
  import { buildSchema, print, getIntrospectionQuery, buildClientSchema, parse as parse$3 } from "graphql";
60
63
  import { diff as diff$1 } from "@graphql-inspector/core";
61
64
  import { processDocumentForIndexing, queryToSearchIndexQuery, optionsToSearchIndexOptions, parseSearchIndexResponse } from "@tinacms/search/index-client";
62
- import { AiFillWarning, AiOutlineLoading } from "react-icons/ai";
63
65
  import { HexColorPicker } from "react-colorful";
64
66
  import { MdKeyboardArrowDown, MdOutlineClear, MdArrowForward, MdAccessTime, MdOutlineDataSaverOff, MdCheckCircle, MdWifiOff, MdOutlineSettings, MdOutlinePhotoLibrary, MdVpnKey, MdImage, MdOutlineCloud, MdWarning, MdInfo, MdError, MdSyncProblem, MdOutlinePerson, MdOutlineHelpOutline } from "react-icons/md";
65
67
  import * as dropzone from "react-dropzone";
@@ -93,8 +95,6 @@ import { VscNewFile } from "react-icons/vsc";
93
95
  import { TbLogs } from "react-icons/tb";
94
96
  import { ImUsers, ImFilesEmpty } from "react-icons/im";
95
97
  import { PiSidebarSimpleLight } from "react-icons/pi";
96
- import { TinaSchema, addNamespaceToSchema, parseURL, resolveForm, normalizePath, canonicalPath, validateSchema } from "@tinacms/schema-tools";
97
- import { NAMER, resolveField } from "@tinacms/schema-tools";
98
98
  import gql from "graphql-tag";
99
99
  import { useLocation, NavLink, useNavigate, useParams, Link as Link$1, HashRouter, Routes, Route } from "react-router-dom";
100
100
  import { RiHome2Line } from "react-icons/ri";
@@ -1356,7 +1356,7 @@ function CodeBlockCombobox({
1356
1356
  const element = useElement();
1357
1357
  const value = element.lang || "plaintext";
1358
1358
  const [searchValue, setSearchValue] = React__default.useState("");
1359
- const items2 = React__default.useMemo(
1359
+ const items = React__default.useMemo(
1360
1360
  () => languages.filter(
1361
1361
  (language) => !searchValue || language.label.toLowerCase().includes(searchValue.toLowerCase())
1362
1362
  ),
@@ -1394,7 +1394,7 @@ function CodeBlockCombobox({
1394
1394
  onValueChange: (value2) => setSearchValue(value2),
1395
1395
  placeholder: "Search language..."
1396
1396
  }
1397
- ), /* @__PURE__ */ React__default.createElement(CommandEmpty$1, null, "No language found."), /* @__PURE__ */ React__default.createElement(CommandList$1, { className: "h-48 overflow-y-auto" }, /* @__PURE__ */ React__default.createElement(CommandGroup$1, null, items2.map((language) => /* @__PURE__ */ React__default.createElement(
1397
+ ), /* @__PURE__ */ React__default.createElement(CommandEmpty$1, null, "No language found."), /* @__PURE__ */ React__default.createElement(CommandList$1, { className: "h-48 overflow-y-auto" }, /* @__PURE__ */ React__default.createElement(CommandGroup$1, null, items.map((language) => /* @__PURE__ */ React__default.createElement(
1398
1398
  CommandItem$1,
1399
1399
  {
1400
1400
  key: language.label,
@@ -2435,16 +2435,16 @@ const InlineCombobox = ({
2435
2435
  const store = useComboboxStore({
2436
2436
  setValue: (newValue) => startTransition(() => setValue(newValue))
2437
2437
  });
2438
- const items2 = store.useState("items");
2438
+ const items = store.useState("items");
2439
2439
  useEffect(() => {
2440
2440
  if (!store.getState().activeId) {
2441
2441
  store.setActiveId(store.first());
2442
2442
  }
2443
- }, [items2, store]);
2443
+ }, [items, store]);
2444
2444
  return /* @__PURE__ */ React__default.createElement("span", { contentEditable: false }, /* @__PURE__ */ React__default.createElement(
2445
2445
  ComboboxProvider,
2446
2446
  {
2447
- open: (items2.length > 0 || hasEmpty) && (!hideWhenNoValue || value.length > 0),
2447
+ open: (items.length > 0 || hasEmpty) && (!hideWhenNoValue || value.length > 0),
2448
2448
  store
2449
2449
  },
2450
2450
  /* @__PURE__ */ React__default.createElement(InlineComboboxContext.Provider, { value: contextValue }, children)
@@ -2546,14 +2546,14 @@ const InlineComboboxEmpty = ({
2546
2546
  }) => {
2547
2547
  const { setHasEmpty } = useContext(InlineComboboxContext);
2548
2548
  const store = useComboboxContext();
2549
- const items2 = store.useState("items");
2549
+ const items = store.useState("items");
2550
2550
  useEffect(() => {
2551
2551
  setHasEmpty(true);
2552
2552
  return () => {
2553
2553
  setHasEmpty(false);
2554
2554
  };
2555
2555
  }, [setHasEmpty]);
2556
- if (items2.length > 0)
2556
+ if (items.length > 0)
2557
2557
  return null;
2558
2558
  return /* @__PURE__ */ React__default.createElement(
2559
2559
  "div",
@@ -2563,28 +2563,80 @@ const InlineComboboxEmpty = ({
2563
2563
  children
2564
2564
  );
2565
2565
  };
2566
- const rules = [
2567
- {
2568
- icon: Icons.h1,
2569
- onSelect: (editor) => {
2570
- editor.tf.toggleBlock(HEADING_KEYS.h1);
2566
+ const ToolbarContext = createContext(
2567
+ void 0
2568
+ );
2569
+ const ToolbarProvider = ({
2570
+ tinaForm,
2571
+ templates,
2572
+ overrides,
2573
+ children
2574
+ }) => {
2575
+ const configured = !Array.isArray(overrides) ? overrides == null ? void 0 : overrides.headingLevels : void 0;
2576
+ const headingLevelsConfigured = Array.isArray(configured);
2577
+ const headingLevels = useMemo(
2578
+ () => configured ? normalizeHeadingLevels(configured) : ALL_HEADING_LEVELS,
2579
+ [configured]
2580
+ );
2581
+ return /* @__PURE__ */ React__default.createElement(
2582
+ ToolbarContext.Provider,
2583
+ {
2584
+ value: {
2585
+ tinaForm,
2586
+ templates,
2587
+ overrides,
2588
+ headingLevels,
2589
+ headingLevelsConfigured
2590
+ }
2571
2591
  },
2592
+ children
2593
+ );
2594
+ };
2595
+ const useToolbarContext = () => {
2596
+ const context = useContext(ToolbarContext);
2597
+ if (!context) {
2598
+ throw new Error("useToolbarContext must be used within a ToolbarProvider");
2599
+ }
2600
+ return context;
2601
+ };
2602
+ const headingRulesByLevel = {
2603
+ h1: {
2604
+ icon: Icons.h1,
2605
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h1),
2572
2606
  value: "Heading 1"
2573
2607
  },
2574
- {
2608
+ h2: {
2575
2609
  icon: Icons.h2,
2576
- onSelect: (editor) => {
2577
- editor.tf.toggleBlock(HEADING_KEYS.h2);
2578
- },
2610
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h2),
2579
2611
  value: "Heading 2"
2580
2612
  },
2581
- {
2613
+ h3: {
2582
2614
  icon: Icons.h3,
2583
- onSelect: (editor) => {
2584
- editor.tf.toggleBlock(HEADING_KEYS.h3);
2585
- },
2615
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h3),
2586
2616
  value: "Heading 3"
2587
2617
  },
2618
+ h4: {
2619
+ icon: Icons.h4,
2620
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h4),
2621
+ value: "Heading 4"
2622
+ },
2623
+ h5: {
2624
+ icon: Icons.h5,
2625
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h5),
2626
+ value: "Heading 5"
2627
+ },
2628
+ h6: {
2629
+ icon: Icons.h6,
2630
+ onSelect: (editor) => editor.tf.toggleBlock(HEADING_KEYS.h6),
2631
+ value: "Heading 6"
2632
+ }
2633
+ };
2634
+ const DEFAULT_SLASH_HEADING_LEVELS = [
2635
+ "h1",
2636
+ "h2",
2637
+ "h3"
2638
+ ];
2639
+ const listRules = [
2588
2640
  {
2589
2641
  icon: Icons.ul,
2590
2642
  keywords: ["ul", "unordered list"],
@@ -2605,6 +2657,12 @@ const rules = [
2605
2657
  const SlashInputElement = withRef(
2606
2658
  ({ className, ...props }, ref) => {
2607
2659
  const { children, editor, element } = props;
2660
+ const { headingLevels, headingLevelsConfigured } = useToolbarContext();
2661
+ const slashHeadingLevels = headingLevelsConfigured ? headingLevels : DEFAULT_SLASH_HEADING_LEVELS;
2662
+ const rules = [
2663
+ ...slashHeadingLevels.map((level) => headingRulesByLevel[level]),
2664
+ ...listRules
2665
+ ];
2608
2666
  useEffect(() => {
2609
2667
  captureEvent(SlashCommandOpenedEvent);
2610
2668
  }, []);
@@ -4426,7 +4484,7 @@ const DropdownButton = React.forwardRef(
4426
4484
  children,
4427
4485
  className = "",
4428
4486
  onMainAction,
4429
- items: items2,
4487
+ items,
4430
4488
  showSplitButton = true,
4431
4489
  ...props
4432
4490
  }, ref) => {
@@ -4453,7 +4511,7 @@ const DropdownButton = React.forwardRef(
4453
4511
  )
4454
4512
  }
4455
4513
  )
4456
- )), /* @__PURE__ */ React.createElement(DropdownMenuContent, { align: "end", side: "bottom", className: "z-[100000]" }, items2.map((item, index) => /* @__PURE__ */ React.createElement(
4514
+ )), /* @__PURE__ */ React.createElement(DropdownMenuContent, { align: "end", side: "bottom", className: "z-[100000]" }, items.map((item, index) => /* @__PURE__ */ React.createElement(
4457
4515
  DropdownMenuItem,
4458
4516
  {
4459
4517
  key: index,
@@ -4500,7 +4558,7 @@ const DropdownButton = React.forwardRef(
4500
4558
  style: { fill: "none" }
4501
4559
  }
4502
4560
  )
4503
- )), /* @__PURE__ */ React.createElement(DropdownMenuContent, { align: "end", side: "bottom" }, items2.map((item, index) => {
4561
+ )), /* @__PURE__ */ React.createElement(DropdownMenuContent, { align: "end", side: "bottom" }, items.map((item, index) => {
4504
4562
  var _a;
4505
4563
  return /* @__PURE__ */ React.createElement(React.Fragment, { key: index }, /* @__PURE__ */ React.createElement(
4506
4564
  DropdownMenuItem,
@@ -4511,7 +4569,7 @@ const DropdownButton = React.forwardRef(
4511
4569
  },
4512
4570
  item.icon && item.icon,
4513
4571
  item.label
4514
- ), item.variant === "destructive" && index < items2.length - 1 && ((_a = items2[index + 1]) == null ? void 0 : _a.variant) !== "destructive" && /* @__PURE__ */ React.createElement(DropdownMenuSeparator, null));
4572
+ ), item.variant === "destructive" && index < items.length - 1 && ((_a = items[index + 1]) == null ? void 0 : _a.variant) !== "destructive" && /* @__PURE__ */ React.createElement(DropdownMenuSeparator, null));
4515
4573
  }))));
4516
4574
  }
4517
4575
  );
@@ -4633,10 +4691,10 @@ const Draggable = ({
4633
4691
  ));
4634
4692
  };
4635
4693
  const SortableProvider = ({
4636
- items: items2,
4694
+ items,
4637
4695
  children
4638
4696
  }) => {
4639
- return /* @__PURE__ */ React__default.createElement(SortableContext, { items: items2, strategy: verticalListSortingStrategy }, children);
4697
+ return /* @__PURE__ */ React__default.createElement(SortableContext, { items, strategy: verticalListSortingStrategy }, children);
4640
4698
  };
4641
4699
  const Dismissible = ({
4642
4700
  onDismiss,
@@ -5590,6 +5648,26 @@ const absoluteImgURL = (str) => {
5590
5648
  return str;
5591
5649
  return `${window.location.origin}${str}`;
5592
5650
  };
5651
+ const MAX_BASENAME_LENGTH = 200;
5652
+ const sanitizeFilename = (filename) => {
5653
+ if (!filename)
5654
+ return "file";
5655
+ const normalized = filename.normalize("NFC");
5656
+ const justName = normalized.split(/[\\/]/).pop() || "";
5657
+ const lastDot = justName.lastIndexOf(".");
5658
+ const hasExt = lastDot > 0 && lastDot < justName.length - 1;
5659
+ const rawBase = hasExt ? justName.slice(0, lastDot) : justName;
5660
+ const rawExt = hasExt ? justName.slice(lastDot) : "";
5661
+ const clean2 = (input) => input.replace(/\s+/g, "-").replace(/[\x00-\x1F\x7F]/g, "").replace(/[<>:"|?*#%&]/g, "-").replace(/-+/g, "-");
5662
+ let base = clean2(rawBase).replace(/^[.\-]+|[.\-]+$/g, "");
5663
+ const ext = clean2(rawExt);
5664
+ if (!base)
5665
+ base = "file";
5666
+ if (base.length > MAX_BASENAME_LENGTH) {
5667
+ base = base.slice(0, MAX_BASENAME_LENGTH).replace(/[.\-]+$/, "") || "file";
5668
+ }
5669
+ return `${base}${ext}`;
5670
+ };
5593
5671
  const { useDropzone: useDropzone$1 } = dropzone;
5594
5672
  const StyledImage = ({ src }) => {
5595
5673
  const isSvg = /\.svg$/.test(src);
@@ -6198,7 +6276,7 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6198
6276
  }
6199
6277
  if (field.openFormOnCreate) {
6200
6278
  const state = tinaForm.finalForm.getState();
6201
- const newIndex = field.addItemBehavior === "prepend" ? 0 : items2.length;
6279
+ const newIndex = field.addItemBehavior === "prepend" ? 0 : items.length;
6202
6280
  if (state.invalid === true) {
6203
6281
  cms.alerts.error("Cannot navigate away from an invalid form.");
6204
6282
  return;
@@ -6216,7 +6294,7 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6216
6294
  });
6217
6295
  }
6218
6296
  }, [form, field]);
6219
- const items2 = input.value || [];
6297
+ const items = input.value || [];
6220
6298
  const itemProps = React__default.useCallback(
6221
6299
  (item) => {
6222
6300
  if (!field.itemProps)
@@ -6225,8 +6303,8 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6225
6303
  },
6226
6304
  [field.itemProps]
6227
6305
  );
6228
- const isMax = items2.length >= (field.max || Number.POSITIVE_INFINITY);
6229
- const isMin = items2.length <= (field.min || 0);
6306
+ const isMax = items.length >= (field.max || Number.POSITIVE_INFINITY);
6307
+ const isMin = items.length <= (field.min || 0);
6230
6308
  const fixedLength = field.min === field.max;
6231
6309
  return /* @__PURE__ */ React__default.createElement(
6232
6310
  ListFieldMeta,
@@ -6249,12 +6327,12 @@ const Group$1 = ({ tinaForm, form, field, input, meta, index }) => {
6249
6327
  /* @__PURE__ */ React__default.createElement(AddIcon, { className: "w-5/6 h-auto" })
6250
6328
  )
6251
6329
  },
6252
- /* @__PURE__ */ React__default.createElement(ListPanel, null, /* @__PURE__ */ React__default.createElement("div", null, /* @__PURE__ */ React__default.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React__default.createElement("div", { ref: provider.innerRef }, items2.length === 0 && /* @__PURE__ */ React__default.createElement(EmptyList, null), /* @__PURE__ */ React__default.createElement(
6330
+ /* @__PURE__ */ React__default.createElement(ListPanel, null, /* @__PURE__ */ React__default.createElement("div", null, /* @__PURE__ */ React__default.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React__default.createElement("div", { ref: provider.innerRef }, items.length === 0 && /* @__PURE__ */ React__default.createElement(EmptyList, null), /* @__PURE__ */ React__default.createElement(
6253
6331
  SortableProvider,
6254
6332
  {
6255
- items: items2.map((_, index2) => `${field.name}.${index2}`)
6333
+ items: items.map((_, index2) => `${field.name}.${index2}`)
6256
6334
  },
6257
- items2.map((item, index2) => /* @__PURE__ */ React__default.createElement(
6335
+ items.map((item, index2) => /* @__PURE__ */ React__default.createElement(
6258
6336
  Item$1,
6259
6337
  {
6260
6338
  key: index2,
@@ -6833,9 +6911,9 @@ const Blocks = ({
6833
6911
  },
6834
6912
  [field.name, form.mutators]
6835
6913
  );
6836
- const items2 = input.value || [];
6837
- const isMax = items2.length >= (field.max || Infinity);
6838
- const isMin = items2.length <= (field.min || 0);
6914
+ const items = input.value || [];
6915
+ const isMax = items.length >= (field.max || Infinity);
6916
+ const isMin = items.length <= (field.min || 0);
6839
6917
  const fixedLength = field.min === field.max;
6840
6918
  return /* @__PURE__ */ React.createElement(
6841
6919
  ListFieldMeta,
@@ -6857,12 +6935,12 @@ const Blocks = ({
6857
6935
  }
6858
6936
  ))
6859
6937
  },
6860
- /* @__PURE__ */ React.createElement(ListPanel, null, /* @__PURE__ */ React.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React.createElement("div", { ref: provider.innerRef, className: "edit-page--list-parent" }, items2.length === 0 && /* @__PURE__ */ React.createElement(EmptyList, null), /* @__PURE__ */ React.createElement(
6938
+ /* @__PURE__ */ React.createElement(ListPanel, null, /* @__PURE__ */ React.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React.createElement("div", { ref: provider.innerRef, className: "edit-page--list-parent" }, items.length === 0 && /* @__PURE__ */ React.createElement(EmptyList, null), /* @__PURE__ */ React.createElement(
6861
6939
  SortableProvider,
6862
6940
  {
6863
- items: items2.map((_, index2) => `${field.name}.${index2}`)
6941
+ items: items.map((_, index2) => `${field.name}.${index2}`)
6864
6942
  },
6865
- items2.map((block, index2) => {
6943
+ items.map((block, index2) => {
6866
6944
  const template = field.templates[block._template];
6867
6945
  if (!template) {
6868
6946
  return /* @__PURE__ */ React.createElement(
@@ -7014,7 +7092,7 @@ const List = ({ tinaForm, form, field, input, meta, index }) => {
7014
7092
  form.mutators.push(field.name, newItem);
7015
7093
  }
7016
7094
  }, [form, field]);
7017
- const items2 = input.value || [];
7095
+ const items = input.value || [];
7018
7096
  const itemProps = React.useCallback(
7019
7097
  (item) => {
7020
7098
  if (!field.itemProps)
@@ -7023,8 +7101,8 @@ const List = ({ tinaForm, form, field, input, meta, index }) => {
7023
7101
  },
7024
7102
  [field.itemProps]
7025
7103
  );
7026
- const isMax = items2.length >= (field == null ? void 0 : field.max);
7027
- const isMin = items2.length <= (field == null ? void 0 : field.min);
7104
+ const isMax = items.length >= (field == null ? void 0 : field.max);
7105
+ const isMin = items.length <= (field == null ? void 0 : field.min);
7028
7106
  const fixedLength = (field == null ? void 0 : field.min) === (field == null ? void 0 : field.max);
7029
7107
  return /* @__PURE__ */ React.createElement(
7030
7108
  ListFieldMeta,
@@ -7046,12 +7124,12 @@ const List = ({ tinaForm, form, field, input, meta, index }) => {
7046
7124
  /* @__PURE__ */ React.createElement(AddIcon, { className: "w-5/6 h-auto" })
7047
7125
  )
7048
7126
  },
7049
- /* @__PURE__ */ React.createElement(ListPanel, null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React.createElement("div", { ref: provider.innerRef }, items2.length === 0 && /* @__PURE__ */ React.createElement(EmptyList, null), /* @__PURE__ */ React.createElement(
7127
+ /* @__PURE__ */ React.createElement(ListPanel, null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(Droppable, { droppableId: field.name, type: field.name }, (provider) => /* @__PURE__ */ React.createElement("div", { ref: provider.innerRef }, items.length === 0 && /* @__PURE__ */ React.createElement(EmptyList, null), /* @__PURE__ */ React.createElement(
7050
7128
  SortableProvider,
7051
7129
  {
7052
- items: items2.map((_, index2) => `${field.name}.${index2}`)
7130
+ items: items.map((_, index2) => `${field.name}.${index2}`)
7053
7131
  },
7054
- items2.map((item, index2) => /* @__PURE__ */ React.createElement(
7132
+ items.map((item, index2) => /* @__PURE__ */ React.createElement(
7055
7133
  Item,
7056
7134
  {
7057
7135
  key: index2,
@@ -7335,8 +7413,8 @@ const TextFieldPlugin = {
7335
7413
  const path = field.name.split(".");
7336
7414
  const fieldName = path[path.length - 1];
7337
7415
  const parent = path.slice(0, path.length - 2);
7338
- const items2 = get(allValues, parent);
7339
- if (((_a = items2 == null ? void 0 : items2.filter((item) => item[fieldName] === value)) == null ? void 0 : _a.length) > 1) {
7416
+ const items = get(allValues, parent);
7417
+ if (((_a = items == null ? void 0 : items.filter((item) => item[fieldName] === value)) == null ? void 0 : _a.length) > 1) {
7340
7418
  return "Item with this unique id already exists";
7341
7419
  }
7342
7420
  }
@@ -7369,7 +7447,7 @@ const TagsField = wrapFieldsWithMeta(({ input, field, form, tinaForm }) => {
7369
7447
  },
7370
7448
  [form, field.name]
7371
7449
  );
7372
- const items2 = input.value || [];
7450
+ const items = input.value || [];
7373
7451
  const ref = React.useRef(null);
7374
7452
  React.useEffect(() => {
7375
7453
  if (ref.current && field.experimental_focusIntent) {
@@ -7402,7 +7480,7 @@ const TagsField = wrapFieldsWithMeta(({ input, field, form, tinaForm }) => {
7402
7480
  className: "flex-shrink-0"
7403
7481
  },
7404
7482
  /* @__PURE__ */ React.createElement(AddIcon, { className: "w-5/6 h-auto" })
7405
- )), /* @__PURE__ */ React.createElement("span", { className: "flex gap-2 flex-wrap mt-2 mb-0" }, items2.length === 0 && /* @__PURE__ */ React.createElement("span", { className: "text-gray-300 text-sm italic" }, "No tags"), items2.map((tag, index) => /* @__PURE__ */ React.createElement(Tag, { key: tag, tinaForm, field, index }, tag))));
7483
+ )), /* @__PURE__ */ React.createElement("span", { className: "flex gap-2 flex-wrap mt-2 mb-0" }, items.length === 0 && /* @__PURE__ */ React.createElement("span", { className: "text-gray-300 text-sm italic" }, "No tags"), items.map((tag, index) => /* @__PURE__ */ React.createElement(Tag, { key: tag, tinaForm, field, index }, tag))));
7406
7484
  });
7407
7485
  const Tag = ({ tinaForm, field, index, children, ...styleProps }) => {
7408
7486
  const removeItem = React.useCallback(() => {
@@ -9228,12 +9306,23 @@ const PullRequestCell = ({
9228
9306
  }
9229
9307
  return null;
9230
9308
  };
9231
- 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 ";
9232
9309
  function formatBranchName(str) {
9233
- const pattern = /[^/\w-]+/g;
9234
- const formattedStr = str.replace(pattern, "-");
9235
- return formattedStr.toLowerCase();
9310
+ let result = "";
9311
+ let replacingInvalidChars = false;
9312
+ for (const char of str.toLowerCase()) {
9313
+ const code = char.charCodeAt(0);
9314
+ const isValid = char === "/" || char === "-" || char === "_" || code >= 48 && code <= 57 || code >= 97 && code <= 122;
9315
+ if (isValid) {
9316
+ result += char;
9317
+ replacingInvalidChars = false;
9318
+ } else if (!replacingInvalidChars) {
9319
+ result += "-";
9320
+ replacingInvalidChars = true;
9321
+ }
9322
+ }
9323
+ return result;
9236
9324
  }
9325
+ 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 ";
9237
9326
  const BranchSwitcher = (props) => {
9238
9327
  const cms = useCMS$1();
9239
9328
  const usingEditorialWorkflow = cms.api.tina.usingEditorialWorkflow;
@@ -9936,9 +10025,28 @@ class EventBus {
9936
10025
  }
9937
10026
  dispatch(event) {
9938
10027
  if (!this.listeners)
9939
- return;
10028
+ return false;
9940
10029
  const listenerSnapshot = Array.from(this.listeners.values());
9941
- listenerSnapshot.forEach((listener) => listener.handleEvent(event));
10030
+ return listenerSnapshot.reduce(
10031
+ (handled, listener) => listener.handleEvent(event) || handled,
10032
+ false
10033
+ );
10034
+ }
10035
+ /**
10036
+ * Whether any listener *explicitly* targets `eventType`. The catch-all `'*'`
10037
+ * pattern does not count — callers use this to detect a purpose-built
10038
+ * subscriber (e.g. a mounted UI overlay) without being misled by ambient
10039
+ * `'*'` listeners such as the alerts bridge, which would otherwise make
10040
+ * {@link dispatch} report every event as "handled".
10041
+ */
10042
+ hasExplicitListenerFor(eventType) {
10043
+ if (!this.listeners)
10044
+ return false;
10045
+ for (const listener of this.listeners) {
10046
+ if (listener.isExplicitListenerFor(eventType))
10047
+ return true;
10048
+ }
10049
+ return false;
9942
10050
  }
9943
10051
  }
9944
10052
  class Listener {
@@ -9953,6 +10061,16 @@ class Listener {
9953
10061
  }
9954
10062
  return false;
9955
10063
  }
10064
+ /**
10065
+ * Whether this listener explicitly targets `eventType`. Unlike
10066
+ * {@link watchesEvent}, the catch-all `'*'` pattern does NOT match, so this
10067
+ * reflects a purpose-built subscription rather than an ambient one.
10068
+ */
10069
+ isExplicitListenerFor(eventType) {
10070
+ if (this.eventPattern === "*")
10071
+ return false;
10072
+ return this.watchesEvent({ type: eventType });
10073
+ }
9956
10074
  watchesEvent(currentEvent) {
9957
10075
  if (this.eventPattern === "*")
9958
10076
  return true;
@@ -9969,23 +10087,83 @@ class Listener {
9969
10087
  return !ignoresEvent;
9970
10088
  }
9971
10089
  }
10090
+ const EDITORIAL_WORKFLOW_STATUS = {
10091
+ QUEUED: "queued",
10092
+ PROCESSING: "processing",
10093
+ SETTING_UP: "setting_up",
10094
+ CREATING_BRANCH: "creating_branch",
10095
+ INDEXING: "indexing",
10096
+ CONTENT_GENERATION: "content_generation",
10097
+ CREATING_PR: "creating_pr",
10098
+ COMPLETE: "complete",
10099
+ ERROR: "error",
10100
+ TIMEOUT: "timeout"
10101
+ };
10102
+ const EDITORIAL_WORKFLOW_ERROR = {
10103
+ BRANCH_EXISTS: "BRANCH_EXISTS",
10104
+ BRANCH_HIERARCHY_CONFLICT: "BRANCH_HIERARCHY_CONFLICT",
10105
+ VALIDATION_FAILED: "VALIDATION_FAILED"
10106
+ };
10107
+ const getEditorialWorkflowPrTitle = (branchName) => `${branchName.replace("tina/", "").replaceAll("-", " ")} (PR from TinaCMS)`;
10108
+ const TARGET_BRANCH_EXISTS_ERROR = "A branch with this name already exists";
10109
+ const checkBranchExists = async (tinaApi, branchName, debugLabel, branchType, fallback, signal) => {
10110
+ try {
10111
+ console.debug(
10112
+ `[tina:branch-guard] ${debugLabel}: checking ${branchType} branch:`,
10113
+ branchName
10114
+ );
10115
+ const exists = await tinaApi.branchExists(branchName, { signal });
10116
+ console.debug(
10117
+ `[tina:branch-guard] ${debugLabel}: ${branchType} branch exists?`,
10118
+ exists
10119
+ );
10120
+ return exists;
10121
+ } catch (err) {
10122
+ if (signal == null ? void 0 : signal.aborted)
10123
+ return fallback;
10124
+ console.error(
10125
+ `[tina:branch-guard] ${debugLabel}: branchExists threw, failing open:`,
10126
+ err
10127
+ );
10128
+ return fallback;
10129
+ }
10130
+ };
10131
+ const checkBaseBranchExists = async (tinaApi, baseBranch, debugLabel, signal) => checkBranchExists(tinaApi, baseBranch, debugLabel, "base", true, signal);
10132
+ const checkTargetBranchExists = async (tinaApi, targetBranch, debugLabel, signal) => checkBranchExists(tinaApi, targetBranch, debugLabel, "target", false, signal);
10133
+ const MEDIA_WORKFLOW_STEP = {
10134
+ BRANCH: 1,
10135
+ CONTENT: 2,
10136
+ PR: 3,
10137
+ COMPLETE: 4
10138
+ };
10139
+ const MEDIA_WORKFLOW_STATUS_TO_STEP = {
10140
+ [EDITORIAL_WORKFLOW_STATUS.SETTING_UP]: MEDIA_WORKFLOW_STEP.BRANCH,
10141
+ [EDITORIAL_WORKFLOW_STATUS.CREATING_BRANCH]: MEDIA_WORKFLOW_STEP.BRANCH,
10142
+ [EDITORIAL_WORKFLOW_STATUS.INDEXING]: MEDIA_WORKFLOW_STEP.CONTENT,
10143
+ [EDITORIAL_WORKFLOW_STATUS.CONTENT_GENERATION]: MEDIA_WORKFLOW_STEP.CONTENT,
10144
+ [EDITORIAL_WORKFLOW_STATUS.CREATING_PR]: MEDIA_WORKFLOW_STEP.PR,
10145
+ [EDITORIAL_WORKFLOW_STATUS.COMPLETE]: MEDIA_WORKFLOW_STEP.COMPLETE
10146
+ };
9972
10147
  const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
9973
10148
  class DummyMediaStore {
9974
10149
  constructor() {
9975
10150
  __publicField(this, "accept", "*");
9976
10151
  }
9977
10152
  async persist(files2) {
9978
- return files2.map(({ directory, file }) => ({
9979
- id: file.name,
9980
- type: "file",
9981
- directory,
9982
- filename: file.name
9983
- }));
10153
+ return files2.map(({ directory, file }) => {
10154
+ const filename = sanitizeFilename(file.name);
10155
+ return {
10156
+ id: filename,
10157
+ type: "file",
10158
+ directory,
10159
+ filename
10160
+ };
10161
+ });
9984
10162
  }
9985
10163
  async list() {
9986
- const items2 = [];
10164
+ const items = [];
9987
10165
  return {
9988
- items: items2,
10166
+ items,
9989
10167
  nextOffset: 0
9990
10168
  };
9991
10169
  }
@@ -10003,6 +10181,9 @@ class TinaMediaStore {
10003
10181
  __publicField(this, "url");
10004
10182
  __publicField(this, "staticMedia");
10005
10183
  __publicField(this, "isStatic");
10184
+ // Route assets-api calls through the created branch until indexing makes it safe to switch React branch state.
10185
+ __publicField(this, "workflowBranchOverride");
10186
+ __publicField(this, "mediaWorkflowInProgress", false);
10006
10187
  __publicField(this, "accept", DEFAULT_MEDIA_UPLOAD_TYPES);
10007
10188
  // allow up to 100MB uploads
10008
10189
  __publicField(this, "maxSize", 100 * 1024 * 1024);
@@ -10042,8 +10223,8 @@ class TinaMediaStore {
10042
10223
  return await this.api.authProvider.isAuthenticated();
10043
10224
  }
10044
10225
  /**
10045
- * Returns the current branch as a single-encoded query-param value, or
10046
- * an empty string when no branch is set.
10226
+ * Returns the workflow branch override or current branch as a single-encoded
10227
+ * query-param value, or an empty string when no branch is set.
10047
10228
  *
10048
10229
  * `this.api.branch` is already URL-encoded by `Client.setBranch()`, so we
10049
10230
  * decode then re-encode here to defend against double-encoding when this
@@ -10056,6 +10237,9 @@ class TinaMediaStore {
10056
10237
  * assets-api (which would route the call to a non-existent staging path).
10057
10238
  */
10058
10239
  encodedBranchParam() {
10240
+ if (this.workflowBranchOverride) {
10241
+ return encodeURIComponent(this.workflowBranchOverride);
10242
+ }
10059
10243
  if (!this.api.branch)
10060
10244
  return "";
10061
10245
  const decoded = decodeURIComponent(this.api.branch);
@@ -10063,66 +10247,297 @@ class TinaMediaStore {
10063
10247
  return "";
10064
10248
  return encodeURIComponent(decoded);
10065
10249
  }
10066
- async persist_cloud(media) {
10067
- if (!await this.isAuthenticated()) {
10068
- return [];
10250
+ shortStableHash(input) {
10251
+ let hash = 2166136261;
10252
+ for (let index = 0; index < input.length; index++) {
10253
+ hash ^= input.charCodeAt(index);
10254
+ hash = Math.imul(hash, 16777619);
10069
10255
  }
10256
+ return (hash >>> 0).toString(36);
10257
+ }
10258
+ trimEdges(value, char) {
10259
+ let start = 0;
10260
+ let end = value.length;
10261
+ while (start < end && value[start] === char) {
10262
+ start++;
10263
+ }
10264
+ while (end > start && value[end - 1] === char) {
10265
+ end--;
10266
+ }
10267
+ return value.slice(start, end);
10268
+ }
10269
+ branchSlugForMediaPath(directory, filename) {
10270
+ const trimmedDirectory = this.trimEdges(directory ?? "", "/");
10271
+ const rawPath = [trimmedDirectory, filename].filter(Boolean).join("/");
10272
+ const flattened = rawPath.replaceAll("/", "-");
10273
+ const slug = this.trimEdges(formatBranchName(flattened), "-");
10274
+ if (!slug)
10275
+ return `asset-${this.shortStableHash(rawPath || "root")}`;
10276
+ return rawPath !== rawPath.toLowerCase() ? `${slug}-${this.shortStableHash(rawPath)}` : slug;
10277
+ }
10278
+ /** Joins a directory and filename into a normalized `dir/file` repo path. */
10279
+ joinMediaPath(directory, filename) {
10280
+ const dir = this.trimEdges(directory ?? "", "/");
10281
+ return dir && dir !== "/" ? `${dir}/${filename ?? ""}` : filename ?? "";
10282
+ }
10283
+ branchQueryParam() {
10070
10284
  const encodedBranch = this.encodedBranchParam();
10071
- const branchQuery = encodedBranch ? `?branch=${encodedBranch}` : "";
10072
- for (const item of media) {
10073
- let directory = item.directory;
10074
- if (directory == null ? void 0 : directory.endsWith("/")) {
10075
- directory = directory.substr(0, directory.length - 1);
10076
- }
10077
- const path = `${directory && directory !== "/" ? `${directory}/${item.file.name}` : item.file.name}`;
10078
- const res = await this.api.authProvider.fetchWithToken(
10079
- `${this.url}/upload_url/${path}${branchQuery}`,
10080
- { method: "GET" }
10285
+ return encodedBranch ? `?branch=${encodedBranch}` : "";
10286
+ }
10287
+ requestMediaBranchChoice(branchName, baseBranch, opType, repoPath) {
10288
+ if (!this.cms.events.hasExplicitListenerFor("media:workflow:confirm-branch")) {
10289
+ throw new Error(
10290
+ "Cannot start a media editorial workflow: no branch prompt is mounted. Ensure <MediaWorkflowOverlay /> is rendered (TinaCloudProvider mounts it automatically)."
10081
10291
  );
10082
- if (res.status === 412) {
10083
- const { message = "Unexpected error generating upload url" } = await res.json();
10084
- throw new Error(message);
10085
- }
10086
- const { signedUrl, requestId } = await res.json();
10087
- if (!signedUrl) {
10088
- throw new Error("Unexpected error generating upload url");
10089
- }
10090
- const uploadRes = await this.fetchFunction(signedUrl, {
10091
- method: "PUT",
10092
- body: item.file,
10093
- headers: {
10094
- "Content-Type": item.file.type || "application/octet-stream",
10095
- "Content-Length": String(item.file.size)
10292
+ }
10293
+ return new Promise((resolve) => {
10294
+ this.cms.events.dispatch({
10295
+ type: "media:workflow:confirm-branch",
10296
+ branchName,
10297
+ baseBranch,
10298
+ onConfirm: async (selectedBranchName) => {
10299
+ const context = await this.prepareMediaBranch(
10300
+ selectedBranchName,
10301
+ baseBranch,
10302
+ opType,
10303
+ repoPath
10304
+ );
10305
+ resolve({
10306
+ kind: "workflow",
10307
+ context
10308
+ });
10309
+ },
10310
+ onCancel: () => resolve({ kind: "cancelled" }),
10311
+ onSaveToProtectedBranch: () => resolve({ kind: "direct" })
10312
+ });
10313
+ });
10314
+ }
10315
+ async prepareMediaBranch(branchName, baseBranch, opType, repoPath) {
10316
+ if (this.mediaWorkflowInProgress) {
10317
+ throw new Error("A media workflow is already in progress.");
10318
+ }
10319
+ this.mediaWorkflowInProgress = true;
10320
+ try {
10321
+ this.cms.events.dispatch({
10322
+ type: "media:workflow:start",
10323
+ branchName,
10324
+ baseBranch
10325
+ });
10326
+ const workflow = await this.api.startMediaEditorialWorkflow({
10327
+ branchName,
10328
+ baseBranch,
10329
+ prTitle: getEditorialWorkflowPrTitle(branchName),
10330
+ operation: opType,
10331
+ repoPath
10332
+ });
10333
+ const branchContext = {
10334
+ branchName: workflow.branchName || branchName,
10335
+ baseBranch,
10336
+ requestId: workflow.requestId
10337
+ };
10338
+ this.workflowBranchOverride = branchContext.branchName;
10339
+ return branchContext;
10340
+ } catch (err) {
10341
+ this.resetWorkflowState();
10342
+ throw err;
10343
+ }
10344
+ }
10345
+ resetWorkflowState() {
10346
+ this.workflowBranchOverride = void 0;
10347
+ this.mediaWorkflowInProgress = false;
10348
+ }
10349
+ async finalizeMediaWorkflow(branchContext, onCatalogued) {
10350
+ try {
10351
+ const result = await this.api.waitForEditorialWorkflowStatus(
10352
+ branchContext.requestId,
10353
+ (status) => {
10354
+ const step = MEDIA_WORKFLOW_STATUS_TO_STEP[status.status];
10355
+ if (step) {
10356
+ this.cms.events.dispatch({
10357
+ type: "media:workflow:step",
10358
+ step
10359
+ });
10360
+ }
10096
10361
  }
10362
+ );
10363
+ if (onCatalogued)
10364
+ await onCatalogued();
10365
+ this.resetWorkflowState();
10366
+ this.cms.events.dispatch({
10367
+ type: "media:workflow:complete",
10368
+ branchName: result.branchName || branchContext.branchName
10097
10369
  });
10098
- if (!uploadRes.ok) {
10099
- const xmlRes = await uploadRes.text();
10100
- const matches = s3ErrorRegex.exec(xmlRes);
10101
- console.error(xmlRes);
10102
- if (!matches) {
10103
- throw new Error("Unexpected error uploading media asset");
10370
+ if (result.warning) {
10371
+ this.cms.alerts.warn(result.warning, 0);
10372
+ }
10373
+ this.cms.alerts.success(
10374
+ `Branch created successfully - Pull Request at ${(result == null ? void 0 : result.pullRequestUrl) || ""}`,
10375
+ 0
10376
+ );
10377
+ this.cms.events.dispatch({ type: "media:workflow:finish" });
10378
+ } catch (err) {
10379
+ this.resetWorkflowState();
10380
+ this.dispatchMediaWorkflowError(err);
10381
+ }
10382
+ }
10383
+ dispatchMediaWorkflowError(err) {
10384
+ this.cms.events.dispatch({
10385
+ type: "media:workflow:error",
10386
+ message: err instanceof Error ? err.message : String(err)
10387
+ });
10388
+ }
10389
+ async runMediaOpWithWorkflow(decision, op) {
10390
+ if (decision.kind !== "workflow")
10391
+ return op();
10392
+ try {
10393
+ const result = await op();
10394
+ await this.finalizeMediaWorkflow(decision.context);
10395
+ return result;
10396
+ } catch (err) {
10397
+ this.resetWorkflowState();
10398
+ this.cms.events.dispatch({ type: "media:workflow:finish" });
10399
+ throw err;
10400
+ }
10401
+ }
10402
+ async prepareProtectedMediaBranch(opType, directory, filename) {
10403
+ if (!this.api.usingProtectedBranch())
10404
+ return { kind: "direct" };
10405
+ const baseBranch = decodeURIComponent(this.api.branch || "");
10406
+ const mediaSlug = this.branchSlugForMediaPath(directory, filename);
10407
+ const branchName = `media-${opType}-${mediaSlug}`;
10408
+ const repoFilename = opType === "upload" && filename ? sanitizeFilename(filename) : filename;
10409
+ const repoPath = this.joinMediaPath(directory, repoFilename);
10410
+ return this.requestMediaBranchChoice(
10411
+ branchName,
10412
+ baseBranch,
10413
+ opType,
10414
+ repoPath
10415
+ );
10416
+ }
10417
+ async waitForRequestStatus(requestId, timeoutMessage) {
10418
+ const startTime = Date.now();
10419
+ while (true) {
10420
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
10421
+ const { error: error2, message } = await this.api.getRequestStatus(requestId);
10422
+ if (error2 !== void 0) {
10423
+ if (error2) {
10424
+ throw new Error(message);
10104
10425
  } else {
10105
- throw new Error(`Upload error: '${matches[2]}'`);
10426
+ return;
10106
10427
  }
10107
10428
  }
10108
- const updateStartTime = Date.now();
10109
- while (true) {
10110
- await new Promise((resolve) => setTimeout(resolve, 1e3));
10111
- const { error: error2, message } = await this.api.getRequestStatus(requestId);
10112
- if (error2 !== void 0) {
10113
- if (error2) {
10114
- throw new Error(message);
10115
- } else {
10116
- break;
10117
- }
10118
- }
10119
- if (Date.now() - updateStartTime > 3e4) {
10120
- throw new Error("Time out waiting for upload to complete");
10121
- }
10429
+ if (Date.now() - startTime > 3e4) {
10430
+ throw new Error(timeoutMessage);
10122
10431
  }
10123
10432
  }
10433
+ }
10434
+ async persist_cloud(media) {
10435
+ if (media.length === 0) {
10436
+ return [];
10437
+ }
10438
+ if (!await this.isAuthenticated()) {
10439
+ return [];
10440
+ }
10441
+ const firstItem = media[0];
10442
+ const decision = await this.prepareProtectedMediaBranch(
10443
+ "upload",
10444
+ firstItem.directory,
10445
+ firstItem.file.name
10446
+ );
10447
+ if (decision.kind === "cancelled")
10448
+ return [];
10449
+ if (decision.kind === "workflow") {
10450
+ return this.persistCloudViaWorkflow(media, decision.context);
10451
+ }
10452
+ const branchQuery = this.branchQueryParam();
10453
+ for (const item of media) {
10454
+ await this.uploadCloudMediaItem(item, branchQuery, {
10455
+ waitForStatus: true
10456
+ });
10457
+ }
10124
10458
  return this.fetchUploadedEntries(media);
10125
10459
  }
10460
+ /**
10461
+ * Uploads assets through the editorial workflow. The asset is staged, the
10462
+ * server-side workflow catalogues it in the new branch's media index, and
10463
+ * only then do we list it — while the branch override still routes list
10464
+ * calls to the workflow branch, so the freshly uploaded item is returned to
10465
+ * the caller (and added to the media manager) without needing a manual
10466
+ * refresh.
10467
+ */
10468
+ async persistCloudViaWorkflow(media, branchContext) {
10469
+ try {
10470
+ const branchQuery = this.branchQueryParam();
10471
+ for (const item of media) {
10472
+ await this.uploadCloudMediaItem(item, branchQuery, {
10473
+ waitForStatus: false
10474
+ });
10475
+ }
10476
+ let entries = [];
10477
+ await this.finalizeMediaWorkflow(branchContext, async () => {
10478
+ entries = await this.fetchUploadedEntries(media);
10479
+ });
10480
+ return entries;
10481
+ } catch (err) {
10482
+ this.resetWorkflowState();
10483
+ this.cms.events.dispatch({ type: "media:workflow:finish" });
10484
+ throw err;
10485
+ }
10486
+ }
10487
+ async uploadCloudMediaItem(item, branchQuery, { waitForStatus = true } = {}) {
10488
+ const path = this.joinMediaPath(
10489
+ item.directory,
10490
+ sanitizeFilename(item.file.name)
10491
+ );
10492
+ const res = await this.api.authProvider.fetchWithToken(
10493
+ `${this.url}/upload_url/${path}${branchQuery}`,
10494
+ { method: "GET" }
10495
+ );
10496
+ if (res.status === 412) {
10497
+ const { message = "Unexpected error generating upload url" } = await res.json();
10498
+ throw new Error(message);
10499
+ }
10500
+ const { signedUrl, requestId } = await res.json();
10501
+ if (!signedUrl) {
10502
+ throw new Error("Unexpected error generating upload url");
10503
+ }
10504
+ const uploadRes = await this.fetchFunction(signedUrl, {
10505
+ method: "PUT",
10506
+ body: item.file,
10507
+ headers: {
10508
+ "Content-Type": item.file.type || "application/octet-stream",
10509
+ "Content-Length": String(item.file.size)
10510
+ }
10511
+ });
10512
+ if (!uploadRes.ok) {
10513
+ const xmlRes = await uploadRes.text();
10514
+ const matches = s3ErrorRegex.exec(xmlRes);
10515
+ console.error(xmlRes);
10516
+ if (!matches) {
10517
+ throw new Error("Unexpected error uploading media asset");
10518
+ } else {
10519
+ throw new Error(`Upload error: '${matches[2]}'`);
10520
+ }
10521
+ }
10522
+ if (waitForStatus) {
10523
+ await this.waitForRequestStatus(
10524
+ requestId,
10525
+ "Time out waiting for upload to complete"
10526
+ );
10527
+ }
10528
+ }
10529
+ groupMediaByDirectory(media) {
10530
+ const byDirectory = /* @__PURE__ */ new Map();
10531
+ for (const item of media) {
10532
+ let dir = item.directory || "";
10533
+ while (dir.endsWith("/"))
10534
+ dir = dir.slice(0, -1);
10535
+ const bucket = byDirectory.get(dir) ?? [];
10536
+ bucket.push(item);
10537
+ byDirectory.set(dir, bucket);
10538
+ }
10539
+ return byDirectory;
10540
+ }
10126
10541
  /**
10127
10542
  * Resolves the just-uploaded items to canonical `Media` entries by hitting
10128
10543
  * the assets-api `list` endpoint, which is the source of truth for the
@@ -10136,27 +10551,19 @@ class TinaMediaStore {
10136
10551
  * throwing — the upload itself already succeeded.
10137
10552
  */
10138
10553
  async fetchUploadedEntries(media) {
10139
- const byDirectory = /* @__PURE__ */ new Map();
10140
- for (const item of media) {
10141
- let dir = item.directory || "";
10142
- while (dir.endsWith("/"))
10143
- dir = dir.slice(0, -1);
10144
- const bucket = byDirectory.get(dir) ?? [];
10145
- bucket.push(item);
10146
- byDirectory.set(dir, bucket);
10147
- }
10554
+ const byDirectory = this.groupMediaByDirectory(media);
10148
10555
  const thumbnailSizes = [
10149
10556
  { w: 75, h: 75 },
10150
10557
  { w: 400, h: 400 },
10151
10558
  { w: 1e3, h: 1e3 }
10152
10559
  ];
10153
10560
  const results = [];
10154
- for (const [directory, items2] of byDirectory) {
10561
+ for (const [directory, items] of byDirectory) {
10155
10562
  let listed;
10156
10563
  try {
10157
10564
  listed = await this.list({
10158
10565
  directory,
10159
- limit: Math.max(100, items2.length * 4),
10566
+ limit: Math.max(100, items.length * 4),
10160
10567
  thumbnailSizes
10161
10568
  });
10162
10569
  } catch (err) {
@@ -10169,8 +10576,8 @@ class TinaMediaStore {
10169
10576
  found.set(entry.filename, entry);
10170
10577
  }
10171
10578
  }
10172
- for (const item of items2) {
10173
- const entry = found.get(item.file.name);
10579
+ for (const item of items) {
10580
+ const entry = found.get(sanitizeFilename(item.file.name));
10174
10581
  if (entry)
10175
10582
  results.push(entry);
10176
10583
  }
@@ -10194,6 +10601,7 @@ class TinaMediaStore {
10194
10601
  }
10195
10602
  for (const item of media) {
10196
10603
  const { file, directory } = item;
10604
+ const safeName = sanitizeFilename(file.name);
10197
10605
  let strippedDirectory = directory;
10198
10606
  if (strippedDirectory.startsWith("/")) {
10199
10607
  strippedDirectory = strippedDirectory.substr(1) || "";
@@ -10202,14 +10610,14 @@ class TinaMediaStore {
10202
10610
  strippedDirectory = strippedDirectory.substr(0, strippedDirectory.length - 1) || "";
10203
10611
  }
10204
10612
  const formData = new FormData();
10205
- formData.append("file", file);
10613
+ formData.append("file", file, safeName);
10206
10614
  formData.append("directory", directory);
10207
- formData.append("filename", file.name);
10208
- let uploadPath = `${strippedDirectory ? `${strippedDirectory}/${file.name}` : file.name}`;
10615
+ formData.append("filename", safeName);
10616
+ let uploadPath = `${strippedDirectory ? `${strippedDirectory}/${safeName}` : safeName}`;
10209
10617
  if (uploadPath.startsWith("/")) {
10210
10618
  uploadPath = uploadPath.substr(1);
10211
10619
  }
10212
- const filePath = `${strippedDirectory ? `${folder}${strippedDirectory}/${file.name}` : folder + file.name}`;
10620
+ const filePath = `${strippedDirectory ? `${folder}${strippedDirectory}/${safeName}` : folder + safeName}`;
10213
10621
  const res = await this.fetchFunction(`${this.url}/upload/${uploadPath}`, {
10214
10622
  method: "POST",
10215
10623
  body: formData
@@ -10222,8 +10630,8 @@ class TinaMediaStore {
10222
10630
  if (fileRes == null ? void 0 : fileRes.success) {
10223
10631
  const parsedRes = {
10224
10632
  type: "file",
10225
- id: file.name,
10226
- filename: file.name,
10633
+ id: safeName,
10634
+ filename: safeName,
10227
10635
  directory,
10228
10636
  src: filePath,
10229
10637
  thumbnails: {
@@ -10311,9 +10719,9 @@ class TinaMediaStore {
10311
10719
  }
10312
10720
  }
10313
10721
  const { cursor, files: files2, directories } = await res.json();
10314
- const items2 = [];
10722
+ const items = [];
10315
10723
  for (const dir of directories) {
10316
- items2.push({
10724
+ items.push({
10317
10725
  type: "dir",
10318
10726
  id: dir,
10319
10727
  directory: options.directory || "",
@@ -10321,7 +10729,7 @@ class TinaMediaStore {
10321
10729
  });
10322
10730
  }
10323
10731
  for (const file of files2) {
10324
- items2.push({
10732
+ items.push({
10325
10733
  directory: options.directory || "",
10326
10734
  type: "file",
10327
10735
  id: file.filename,
@@ -10334,42 +10742,38 @@ class TinaMediaStore {
10334
10742
  });
10335
10743
  }
10336
10744
  return {
10337
- items: items2,
10745
+ items,
10338
10746
  nextOffset: cursor || 0
10339
10747
  };
10340
10748
  }
10341
10749
  async delete(media) {
10342
- const path = `${media.directory ? `${media.directory}/${media.filename}` : media.filename}`;
10750
+ const path = this.joinMediaPath(media.directory, media.filename);
10343
10751
  if (!this.isLocal) {
10344
10752
  if (await this.isAuthenticated()) {
10345
- const encodedBranch = this.encodedBranchParam();
10346
- const branchQuery = encodedBranch ? `?branch=${encodedBranch}` : "";
10347
- const res = await this.api.authProvider.fetchWithToken(
10348
- `${this.url}/${path}${branchQuery}`,
10349
- {
10350
- method: "DELETE"
10351
- }
10753
+ const decision = await this.prepareProtectedMediaBranch(
10754
+ "delete",
10755
+ media.directory,
10756
+ media.filename
10352
10757
  );
10353
- if (res.status == 200) {
10758
+ if (decision.kind === "cancelled")
10759
+ return;
10760
+ await this.runMediaOpWithWorkflow(decision, async () => {
10761
+ const branchQuery = this.branchQueryParam();
10762
+ const res = await this.api.authProvider.fetchWithToken(
10763
+ `${this.url}/${path}${branchQuery}`,
10764
+ { method: "DELETE" }
10765
+ );
10766
+ if (res.status !== 200) {
10767
+ throw new Error("Unexpected error deleting media asset");
10768
+ }
10354
10769
  const { requestId } = await res.json();
10355
- const deleteStartTime = Date.now();
10356
- while (true) {
10357
- await new Promise((resolve) => setTimeout(resolve, 1e3));
10358
- const { error: error2, message } = await this.api.getRequestStatus(requestId);
10359
- if (error2 !== void 0) {
10360
- if (error2) {
10361
- throw new Error(message);
10362
- } else {
10363
- break;
10364
- }
10365
- }
10366
- if (Date.now() - deleteStartTime > 3e4) {
10367
- throw new Error("Time out waiting for delete to complete");
10368
- }
10770
+ if (decision.kind !== "workflow") {
10771
+ await this.waitForRequestStatus(
10772
+ requestId,
10773
+ "Time out waiting for delete to complete"
10774
+ );
10369
10775
  }
10370
- } else {
10371
- throw new Error("Unexpected error deleting media asset");
10372
- }
10776
+ });
10373
10777
  } else {
10374
10778
  throw E_UNAUTHORIZED;
10375
10779
  }
@@ -12011,14 +12415,16 @@ const MediaLightbox = ({
12011
12415
  item,
12012
12416
  onClose
12013
12417
  }) => {
12418
+ useEffect(() => {
12419
+ if (!item)
12420
+ return;
12421
+ captureEvent(MediaUsageDashboardPreviewOpenedEvent);
12422
+ }, [item]);
12014
12423
  if (!item)
12015
12424
  return null;
12016
12425
  const usageCount = item.usedIn.length;
12017
12426
  const mediaSrc = item.media.src;
12018
12427
  const directory = item.media.directory || "/";
12019
- useEffect(() => {
12020
- captureEvent(MediaUsageDashboardPreviewOpenedEvent);
12021
- }, []);
12022
12428
  return /* @__PURE__ */ React__default.createElement(Dialog, { open: true, onOpenChange: (isOpen) => !isOpen && onClose() }, /* @__PURE__ */ React__default.createElement(DialogContent, { className: "w-auto max-w-[95vw] border border-gray-200 bg-white px-4 pt-12 pb-4 shadow-xl sm:max-w-fit" }, /* @__PURE__ */ React__default.createElement(DialogTitle, { className: "sr-only" }, "Preview: ", item.media.filename), /* @__PURE__ */ React__default.createElement(DialogDescription, { className: "sr-only" }, usageCount > 0 ? `Used in ${usageCount} ${usageCount === 1 ? "document" : "documents"}` : "Unused media file"), /* @__PURE__ */ React__default.createElement("div", { className: "mx-auto w-fit max-w-full rounded-lg border border-gray-200 bg-gray-50 p-2 sm:p-3" }, item.type === "video" ? /* @__PURE__ */ React__default.createElement(
12023
12429
  VideoLightboxContent,
12024
12430
  {
@@ -12780,7 +13186,7 @@ const NavProvider = ({
12780
13186
  const name = "tinacms";
12781
13187
  const type = "module";
12782
13188
  const typings = "dist/index.d.ts";
12783
- const version$1 = "3.8.3";
13189
+ const version$1 = "3.9.0";
12784
13190
  const main = "dist/index.js";
12785
13191
  const module = "./dist/index.js";
12786
13192
  const exports = {
@@ -13511,7 +13917,7 @@ function BreadcrumbEllipsis({
13511
13917
  );
13512
13918
  }
13513
13919
  const getPaddingClass = (depth) => `${1.5 + depth * 1.35}rem`;
13514
- const collectAllDocumentItems = (items2, isGlobalFn) => {
13920
+ const collectAllDocumentItems = (items, isGlobalFn) => {
13515
13921
  const allItems = [];
13516
13922
  const processItem = (item, isFromSubItems = false) => {
13517
13923
  if (item.type === "document") {
@@ -13526,13 +13932,13 @@ const collectAllDocumentItems = (items2, isGlobalFn) => {
13526
13932
  }
13527
13933
  }
13528
13934
  };
13529
- items2.forEach((item) => processItem(item, false));
13935
+ items.forEach((item) => processItem(item, false));
13530
13936
  return allItems;
13531
13937
  };
13532
- const buildTreeFromPaths = (items2) => {
13938
+ const buildTreeFromPaths = (items) => {
13533
13939
  const root = [];
13534
13940
  const nodeMap = /* @__PURE__ */ new Map();
13535
- items2.forEach((item) => {
13941
+ items.forEach((item) => {
13536
13942
  const parts = item.formId.split("/").filter(Boolean);
13537
13943
  let currentLevel = root;
13538
13944
  let currentPath = "";
@@ -15369,23 +15775,6 @@ const BranchPreviewButton = (props) => {
15369
15775
  /* @__PURE__ */ React.createElement(BiLinkExternal, { className: "h-5 w-auto" })
15370
15776
  );
15371
15777
  };
15372
- const EDITORIAL_WORKFLOW_STATUS = {
15373
- QUEUED: "queued",
15374
- PROCESSING: "processing",
15375
- SETTING_UP: "setting_up",
15376
- CREATING_BRANCH: "creating_branch",
15377
- INDEXING: "indexing",
15378
- CONTENT_GENERATION: "content_generation",
15379
- CREATING_PR: "creating_pr",
15380
- COMPLETE: "complete",
15381
- ERROR: "error",
15382
- TIMEOUT: "timeout"
15383
- };
15384
- const EDITORIAL_WORKFLOW_ERROR = {
15385
- BRANCH_EXISTS: "BRANCH_EXISTS",
15386
- BRANCH_HIERARCHY_CONFLICT: "BRANCH_HIERARCHY_CONFLICT",
15387
- VALIDATION_FAILED: "VALIDATION_FAILED"
15388
- };
15389
15778
  const CREATE_DOCUMENT_GQL = `#graphql
15390
15779
  mutation($collection: String!, $relativePath: String!, $params: DocumentMutation!) {
15391
15780
  createDocument(
@@ -15743,6 +16132,18 @@ const pathRelativeToCollection = (collectionPath, fullPath) => {
15743
16132
  `Path ${fullPath} not within collection path ${collectionPath}`
15744
16133
  );
15745
16134
  };
16135
+ const getEditorialWorkflowMutation = (crudType) => {
16136
+ if (crudType === "create") {
16137
+ return CREATE_DOCUMENT_GQL;
16138
+ }
16139
+ if (crudType === "delete") {
16140
+ return DELETE_DOCUMENT_GQL;
16141
+ }
16142
+ if (crudType !== "view") {
16143
+ return UPDATE_DOCUMENT_GQL;
16144
+ }
16145
+ return "";
16146
+ };
15746
16147
  const WORKFLOW_STEPS = [
15747
16148
  { id: 1, name: "Creating branch", description: "Setting up workspace" },
15748
16149
  { id: 2, name: "Updating branch", description: "Syncing content to branch" },
@@ -15753,6 +16154,33 @@ const formatTime = (seconds) => {
15753
16154
  const secs = seconds % 60;
15754
16155
  return `${mins}:${secs.toString().padStart(2, "0")}`;
15755
16156
  };
16157
+ const getEditorialWorkflowErrorMessage = (e) => {
16158
+ let errMessage = "Branch operation failed. Talking to GitHub was unsuccessful, please try again. If the problem persists please contact support at https://tina.io/support 🦙";
16159
+ const err = e;
16160
+ if (err.errorCode) {
16161
+ switch (err.errorCode) {
16162
+ case EDITORIAL_WORKFLOW_ERROR.BRANCH_EXISTS:
16163
+ errMessage = "A branch with this name already exists";
16164
+ break;
16165
+ case EDITORIAL_WORKFLOW_ERROR.BRANCH_HIERARCHY_CONFLICT:
16166
+ errMessage = err.message || "Branch name conflicts with an existing branch";
16167
+ break;
16168
+ case EDITORIAL_WORKFLOW_ERROR.VALIDATION_FAILED:
16169
+ errMessage = err.message || "Invalid branch name";
16170
+ break;
16171
+ default:
16172
+ errMessage = err.message || errMessage;
16173
+ break;
16174
+ }
16175
+ } else if (err.message) {
16176
+ if (err.message.toLowerCase().includes("already exists")) {
16177
+ errMessage = "A branch with this name already exists";
16178
+ } else if (err.message.toLowerCase().includes("conflict")) {
16179
+ errMessage = err.message;
16180
+ }
16181
+ }
16182
+ return errMessage;
16183
+ };
15756
16184
  function useEditorialWorkflow() {
15757
16185
  const cms = useCMS$1();
15758
16186
  const tinaApi = cms.api.tina;
@@ -15786,20 +16214,30 @@ function useEditorialWorkflow() {
15786
16214
  path,
15787
16215
  values,
15788
16216
  crudType,
15789
- tinaForm
16217
+ tinaForm,
16218
+ signal
15790
16219
  }) => {
15791
16220
  var _a;
15792
16221
  try {
16222
+ if (signal == null ? void 0 : signal.aborted)
16223
+ return false;
16224
+ const targetBranchExists = await checkTargetBranchExists(
16225
+ tinaApi,
16226
+ branchName,
16227
+ "executeEditorialWorkflow",
16228
+ signal
16229
+ );
16230
+ if (signal == null ? void 0 : signal.aborted)
16231
+ return false;
16232
+ if (targetBranchExists) {
16233
+ setErrorMessage(TARGET_BRANCH_EXISTS_ERROR);
16234
+ setIsExecuting(false);
16235
+ setCurrentStep(0);
16236
+ return false;
16237
+ }
15793
16238
  setIsExecuting(true);
15794
16239
  setCurrentStep(1);
15795
- let graphql2 = "";
15796
- if (crudType === "create") {
15797
- graphql2 = CREATE_DOCUMENT_GQL;
15798
- } else if (crudType === "delete") {
15799
- graphql2 = DELETE_DOCUMENT_GQL;
15800
- } else if (crudType !== "view") {
15801
- graphql2 = UPDATE_DOCUMENT_GQL;
15802
- }
16240
+ const graphql2 = getEditorialWorkflowMutation(crudType);
15803
16241
  const collection = tinaApi.schema.getCollectionByFullPath(path);
15804
16242
  let submittedValues = values;
15805
16243
  if ((_a = collection == null ? void 0 : collection.ui) == null ? void 0 : _a.beforeSubmit) {
@@ -15820,7 +16258,7 @@ function useEditorialWorkflow() {
15820
16258
  const result = await tinaApi.executeEditorialWorkflow({
15821
16259
  branchName,
15822
16260
  baseBranch,
15823
- prTitle: `${branchName.replace("tina/", "").replace("-", " ")} (PR from TinaCMS)`,
16261
+ prTitle: getEditorialWorkflowPrTitle(branchName),
15824
16262
  graphQLContentOp: {
15825
16263
  query: graphql2,
15826
16264
  variables: {
@@ -15869,30 +16307,7 @@ function useEditorialWorkflow() {
15869
16307
  return true;
15870
16308
  } catch (e) {
15871
16309
  console.error(e);
15872
- let errMessage = "Branch operation failed. Talking to GitHub was unsuccessful, please try again. If the problem persists please contact support at https://tina.io/support 🦙";
15873
- const err = e;
15874
- if (err.errorCode) {
15875
- switch (err.errorCode) {
15876
- case EDITORIAL_WORKFLOW_ERROR.BRANCH_EXISTS:
15877
- errMessage = "A branch with this name already exists";
15878
- break;
15879
- case EDITORIAL_WORKFLOW_ERROR.BRANCH_HIERARCHY_CONFLICT:
15880
- errMessage = err.message || "Branch name conflicts with an existing branch";
15881
- break;
15882
- case EDITORIAL_WORKFLOW_ERROR.VALIDATION_FAILED:
15883
- errMessage = err.message || "Invalid branch name";
15884
- break;
15885
- default:
15886
- errMessage = err.message || errMessage;
15887
- break;
15888
- }
15889
- } else if (err.message) {
15890
- if (err.message.toLowerCase().includes("already exists")) {
15891
- errMessage = "A branch with this name already exists";
15892
- } else if (err.message.toLowerCase().includes("conflict")) {
15893
- errMessage = err.message;
15894
- }
15895
- }
16310
+ const errMessage = getEditorialWorkflowErrorMessage(e);
15896
16311
  setErrorMessage(errMessage);
15897
16312
  setIsExecuting(false);
15898
16313
  setCurrentStep(0);
@@ -15980,6 +16395,18 @@ const WorkflowProgressIndicator = ({
15980
16395
  "Learn more about Editorial Workflow"
15981
16396
  ));
15982
16397
  };
16398
+ const EditorialWorkflowProgressModal = ({
16399
+ title,
16400
+ currentStep,
16401
+ elapsedTime
16402
+ }) => /* @__PURE__ */ React.createElement(Modal, { className: "flex" }, /* @__PURE__ */ React.createElement(PopupModal, { className: "w-auto" }, /* @__PURE__ */ React.createElement(ModalHeader, null, title), /* @__PURE__ */ React.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React.createElement(
16403
+ WorkflowProgressIndicator,
16404
+ {
16405
+ currentStep,
16406
+ isExecuting: true,
16407
+ elapsedTime
16408
+ }
16409
+ ))));
15983
16410
  const formatDefaultBranchName = (filePath, crudType) => {
15984
16411
  let result = filePath;
15985
16412
  const contentPrefix = "content/";
@@ -16011,6 +16438,7 @@ const CreateBranchModal = ({
16011
16438
  formatDefaultBranchName(path, crudType)
16012
16439
  );
16013
16440
  const [isBranchGuardChecking, setIsBranchGuardChecking] = React.useState(false);
16441
+ const branchGuardAbortRef = React.useRef(null);
16014
16442
  const {
16015
16443
  isExecuting,
16016
16444
  errorMessage,
@@ -16019,27 +16447,35 @@ const CreateBranchModal = ({
16019
16447
  executeWorkflow,
16020
16448
  reset
16021
16449
  } = useEditorialWorkflow();
16450
+ const abortBranchGuard = React.useCallback(() => {
16451
+ var _a;
16452
+ (_a = branchGuardAbortRef.current) == null ? void 0 : _a.abort();
16453
+ branchGuardAbortRef.current = null;
16454
+ setIsBranchGuardChecking(false);
16455
+ }, []);
16456
+ React.useEffect(() => {
16457
+ return () => {
16458
+ var _a;
16459
+ (_a = branchGuardAbortRef.current) == null ? void 0 : _a.abort();
16460
+ };
16461
+ }, []);
16022
16462
  const executeEditorialWorkflow = async () => {
16463
+ abortBranchGuard();
16464
+ const abortController = new AbortController();
16465
+ branchGuardAbortRef.current = abortController;
16023
16466
  setIsBranchGuardChecking(true);
16024
16467
  const baseBranch = decodeURIComponent(tinaApi.branch);
16025
- let baseBranchExists = true;
16026
- try {
16027
- console.debug(
16028
- "[tina:branch-guard] executeEditorialWorkflow: checking base branch:",
16029
- baseBranch
16030
- );
16031
- baseBranchExists = await tinaApi.branchExists(baseBranch);
16032
- } catch (err) {
16033
- console.error(
16034
- "[tina:branch-guard] executeEditorialWorkflow: branchExists threw, failing open:",
16035
- err
16036
- );
16037
- }
16038
- console.debug(
16039
- "[tina:branch-guard] executeEditorialWorkflow: base branch exists?",
16040
- baseBranchExists
16468
+ const targetBranch = `tina/${newBranchName}`;
16469
+ const baseBranchExists = await checkBaseBranchExists(
16470
+ tinaApi,
16471
+ baseBranch,
16472
+ "executeEditorialWorkflow",
16473
+ abortController.signal
16041
16474
  );
16475
+ if (abortController.signal.aborted)
16476
+ return;
16042
16477
  if (!baseBranchExists) {
16478
+ abortBranchGuard();
16043
16479
  console.debug(
16044
16480
  "[tina:branch-guard] executeEditorialWorkflow: base branch deleted — handing off"
16045
16481
  );
@@ -16048,52 +16484,84 @@ const CreateBranchModal = ({
16048
16484
  }
16049
16485
  setIsBranchGuardChecking(false);
16050
16486
  const success = await executeWorkflow({
16051
- branchName: `tina/${newBranchName}`,
16487
+ branchName: targetBranch,
16052
16488
  baseBranch,
16053
16489
  path,
16054
16490
  values,
16055
16491
  crudType,
16056
- tinaForm
16492
+ tinaForm,
16493
+ signal: abortController.signal
16057
16494
  });
16495
+ if (branchGuardAbortRef.current === abortController) {
16496
+ branchGuardAbortRef.current = null;
16497
+ }
16058
16498
  if (success) {
16059
16499
  close2();
16060
16500
  }
16061
16501
  };
16062
- const renderStateContent = () => {
16063
- if (isExecuting) {
16064
- return /* @__PURE__ */ React.createElement(
16065
- WorkflowProgressIndicator,
16066
- {
16067
- currentStep,
16068
- isExecuting,
16069
- elapsedTime
16070
- }
16071
- );
16072
- } else {
16073
- return /* @__PURE__ */ React.createElement("div", { className: "max-w-sm" }, errorMessage && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 text-red-700 py-2 px-3 mb-4 bg-red-50 border border-red-200 rounded" }, /* @__PURE__ */ React.createElement(BiError, { className: "w-5 h-auto text-red-400 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm" }, /* @__PURE__ */ React.createElement("b", null, "Error:"), " ", errorMessage)), /* @__PURE__ */ React.createElement("p", { className: "text-lg text-gray-700 font-bold mb-2" }, "First, let's create a copy"), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-gray-700 mb-4 max-w-sm" }, "To make changes, you need to create a copy then get it approved and merged for it to go live.", /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("span", { className: "text-gray-500" }, "Learn more about "), /* @__PURE__ */ React.createElement(
16074
- "a",
16075
- {
16076
- className: "underline text-tina-orange-dark font-medium",
16077
- href: "https://tina.io/docs/r/editorial-workflow",
16078
- target: "_blank"
16079
- },
16080
- "Editorial Workflow"
16081
- ), "."), /* @__PURE__ */ React.createElement(
16082
- PrefixedTextField,
16083
- {
16084
- name: "new-branch-name",
16085
- label: "Branch Name",
16086
- placeholder: "e.g. {{PAGE-NAME}}-updates",
16087
- value: newBranchName,
16088
- onChange: (e) => {
16089
- reset();
16090
- setNewBranchName(e.target.value);
16091
- }
16092
- }
16093
- ));
16502
+ if (isExecuting) {
16503
+ return /* @__PURE__ */ React.createElement(
16504
+ EditorialWorkflowProgressModal,
16505
+ {
16506
+ title: "Save changes to new branch",
16507
+ currentStep,
16508
+ elapsedTime
16509
+ }
16510
+ );
16511
+ }
16512
+ return /* @__PURE__ */ React.createElement(
16513
+ CreateBranchPromptModal,
16514
+ {
16515
+ branchName: newBranchName,
16516
+ close: () => {
16517
+ abortBranchGuard();
16518
+ close2();
16519
+ },
16520
+ errorMessage,
16521
+ disabled: newBranchName === "" || isBranchGuardChecking,
16522
+ onBranchNameChange: (value) => {
16523
+ abortBranchGuard();
16524
+ reset();
16525
+ setNewBranchName(value);
16526
+ },
16527
+ onCreateBranch: executeEditorialWorkflow,
16528
+ onSaveToProtectedBranch: () => {
16529
+ abortBranchGuard();
16530
+ close2();
16531
+ safeSubmit();
16532
+ }
16094
16533
  }
16095
- };
16096
- return /* @__PURE__ */ React.createElement(Modal, { className: "flex" }, /* @__PURE__ */ React.createElement(PopupModal, { className: "w-auto" }, /* @__PURE__ */ React.createElement(ModalHeader, { close: isExecuting ? void 0 : close2 }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between w-full" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, "Save changes to new branch"))), /* @__PURE__ */ React.createElement(ModalBody, { padded: true }, renderStateContent()), !isExecuting && /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
16534
+ );
16535
+ };
16536
+ const CreateBranchPromptModal = ({
16537
+ branchName,
16538
+ close: close2,
16539
+ disabled,
16540
+ errorMessage,
16541
+ onBranchNameChange,
16542
+ onCreateBranch,
16543
+ onSaveToProtectedBranch
16544
+ }) => {
16545
+ return /* @__PURE__ */ React.createElement(Modal, { className: "flex" }, /* @__PURE__ */ React.createElement(PopupModal, { className: "w-auto" }, /* @__PURE__ */ React.createElement(ModalHeader, { close: close2 }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between w-full" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, "Save changes to new branch"))), /* @__PURE__ */ React.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React.createElement("div", { className: "max-w-sm" }, errorMessage && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 text-red-700 py-2 px-3 mb-4 bg-red-50 border border-red-200 rounded" }, /* @__PURE__ */ React.createElement(BiError, { className: "w-5 h-auto text-red-400 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm" }, /* @__PURE__ */ React.createElement("b", null, "Error:"), " ", errorMessage)), /* @__PURE__ */ React.createElement("p", { className: "text-lg text-gray-700 font-bold mb-2" }, "First, let's create a copy"), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-gray-700 mb-4 max-w-sm" }, "To make changes, you need to create a copy then get it approved and merged for it to go live.", /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("span", { className: "text-gray-500" }, "Learn more about "), /* @__PURE__ */ React.createElement(
16546
+ "a",
16547
+ {
16548
+ className: "underline text-tina-orange-dark font-medium",
16549
+ href: "https://tina.io/docs/r/editorial-workflow",
16550
+ target: "_blank"
16551
+ },
16552
+ "Editorial Workflow"
16553
+ ), "."), /* @__PURE__ */ React.createElement(
16554
+ PrefixedTextField,
16555
+ {
16556
+ name: "new-branch-name",
16557
+ label: "Branch Name",
16558
+ placeholder: "e.g. {{PAGE-NAME}}-updates",
16559
+ value: branchName,
16560
+ onChange: (e) => {
16561
+ onBranchNameChange(e.target.value);
16562
+ }
16563
+ }
16564
+ ))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
16097
16565
  Button$2,
16098
16566
  {
16099
16567
  variant: "secondary",
@@ -16107,26 +16575,17 @@ const CreateBranchModal = ({
16107
16575
  variant: "primary",
16108
16576
  align: "start",
16109
16577
  className: "w-full sm:w-auto",
16110
- disabled: newBranchName === "" || isBranchGuardChecking,
16111
- onMainAction: executeEditorialWorkflow,
16578
+ disabled,
16579
+ onMainAction: onCreateBranch,
16112
16580
  items: [
16113
16581
  {
16114
16582
  label: "Save to Protected Branch",
16115
- onClick: () => {
16116
- close2();
16117
- safeSubmit();
16118
- },
16583
+ onClick: onSaveToProtectedBranch,
16119
16584
  icon: /* @__PURE__ */ React.createElement(TriangleAlert, { className: "w-4 h-4" })
16120
16585
  }
16121
16586
  ]
16122
16587
  },
16123
- /* @__PURE__ */ React.createElement(
16124
- GitBranchIcon,
16125
- {
16126
- className: "w-4 h-4 mr-1",
16127
- style: { fill: "none" }
16128
- }
16129
- ),
16588
+ /* @__PURE__ */ React.createElement(GitBranchIcon, { className: "w-4 h-4 mr-1", style: { fill: "none" } }),
16130
16589
  "Save to a new branch"
16131
16590
  ))));
16132
16591
  };
@@ -17004,43 +17463,15 @@ const formatList = (editor, elementType) => {
17004
17463
  })
17005
17464
  );
17006
17465
  };
17007
- const autoformatBlocks = [
17008
- {
17009
- mode: "block",
17010
- type: HEADING_KEYS.h1,
17011
- match: "# ",
17012
- preFormat
17013
- },
17014
- {
17015
- mode: "block",
17016
- type: HEADING_KEYS.h2,
17017
- match: "## ",
17018
- preFormat
17019
- },
17020
- {
17021
- mode: "block",
17022
- type: HEADING_KEYS.h3,
17023
- match: "### ",
17024
- preFormat
17025
- },
17026
- {
17027
- mode: "block",
17028
- type: HEADING_KEYS.h4,
17029
- match: "#### ",
17030
- preFormat
17031
- },
17032
- {
17033
- mode: "block",
17034
- type: HEADING_KEYS.h5,
17035
- match: "##### ",
17036
- preFormat
17037
- },
17038
- {
17039
- mode: "block",
17040
- type: HEADING_KEYS.h6,
17041
- match: "###### ",
17042
- preFormat
17043
- },
17466
+ const headingAutoformatByLevel = {
17467
+ h1: { mode: "block", type: HEADING_KEYS.h1, match: "# ", preFormat },
17468
+ h2: { mode: "block", type: HEADING_KEYS.h2, match: "## ", preFormat },
17469
+ h3: { mode: "block", type: HEADING_KEYS.h3, match: "### ", preFormat },
17470
+ h4: { mode: "block", type: HEADING_KEYS.h4, match: "#### ", preFormat },
17471
+ h5: { mode: "block", type: HEADING_KEYS.h5, match: "##### ", preFormat },
17472
+ h6: { mode: "block", type: HEADING_KEYS.h6, match: "###### ", preFormat }
17473
+ };
17474
+ const nonHeadingAutoformatBlocks = [
17044
17475
  {
17045
17476
  mode: "block",
17046
17477
  type: BlockquotePlugin.key,
@@ -17072,6 +17503,16 @@ const autoformatBlocks = [
17072
17503
  }
17073
17504
  }
17074
17505
  ];
17506
+ const getAutoformatBlocks = (headingLevels = ALL_HEADING_LEVELS) => [
17507
+ // Normalize so a pure-JS schema passing e.g. `['h7']` doesn't push an
17508
+ // `undefined` rule into the autoformat config. The toolbar context
17509
+ // applies the same filter, keeping both surfaces consistent.
17510
+ ...normalizeHeadingLevels(headingLevels).map(
17511
+ (level) => headingAutoformatByLevel[level]
17512
+ ),
17513
+ ...nonHeadingAutoformatBlocks
17514
+ ];
17515
+ const autoformatBlocks = getAutoformatBlocks();
17075
17516
  const autoformatLists = [
17076
17517
  {
17077
17518
  mode: "block",
@@ -17449,70 +17890,36 @@ const HEADING_ICON_ONLY = 58;
17449
17890
  const EMBED_ICON_WIDTH = 78;
17450
17891
  const CONTAINER_MD_BREAKPOINT = 448;
17451
17892
  const HEADING_LABEL = "Headings";
17452
- const ToolbarContext = createContext(
17453
- void 0
17454
- );
17455
- const ToolbarProvider = ({
17456
- tinaForm,
17457
- templates,
17458
- overrides,
17459
- children
17460
- }) => {
17461
- return /* @__PURE__ */ React__default.createElement(ToolbarContext.Provider, { value: { tinaForm, templates, overrides } }, children);
17462
- };
17463
- const useToolbarContext = () => {
17464
- const context = useContext(ToolbarContext);
17465
- if (!context) {
17466
- throw new Error("useToolbarContext must be used within a ToolbarProvider");
17467
- }
17468
- return context;
17893
+ const buildByLevel = (make) => ({
17894
+ h1: make("h1"),
17895
+ h2: make("h2"),
17896
+ h3: make("h3"),
17897
+ h4: make("h4"),
17898
+ h5: make("h5"),
17899
+ h6: make("h6")
17900
+ });
17901
+ const headingItemsByLevel = buildByLevel((level) => {
17902
+ const depth = level.slice(1);
17903
+ return {
17904
+ description: `Heading ${depth}`,
17905
+ icon: Icons[level],
17906
+ label: `Heading ${depth}`,
17907
+ value: level
17908
+ };
17909
+ });
17910
+ const paragraphItem = {
17911
+ description: "Paragraph",
17912
+ icon: Icons.paragraph,
17913
+ label: "Paragraph",
17914
+ value: ParagraphPlugin.key
17469
17915
  };
17470
- const items$1 = [
17471
- {
17472
- description: "Paragraph",
17473
- icon: Icons.heading,
17474
- label: "Paragraph",
17475
- value: ParagraphPlugin.key
17476
- },
17477
- {
17478
- description: "Heading 1",
17479
- icon: Icons.h1,
17480
- label: "Heading 1",
17481
- value: HEADING_KEYS.h1
17482
- },
17483
- {
17484
- description: "Heading 2",
17485
- icon: Icons.h2,
17486
- label: "Heading 2",
17487
- value: HEADING_KEYS.h2
17488
- },
17489
- {
17490
- description: "Heading 3",
17491
- icon: Icons.h3,
17492
- label: "Heading 3",
17493
- value: HEADING_KEYS.h3
17494
- },
17495
- {
17496
- description: "Heading 4",
17497
- icon: Icons.h4,
17498
- label: "Heading 4",
17499
- value: HEADING_KEYS.h4
17500
- },
17501
- {
17502
- description: "Heading 5",
17503
- icon: Icons.h5,
17504
- label: "Heading 5",
17505
- value: HEADING_KEYS.h5
17506
- },
17507
- {
17508
- description: "Heading 6",
17509
- icon: Icons.h6,
17510
- label: "Heading 6",
17511
- value: HEADING_KEYS.h6
17512
- }
17513
- ];
17514
- const defaultItem$1 = items$1.find((item) => item.value === ParagraphPlugin.key) || items$1[0];
17916
+ const getHeadingItem = (value) => isHeadingLevel(value) ? headingItemsByLevel[value] : void 0;
17515
17917
  function HeadingsMenu(props) {
17918
+ const { headingLevels } = useToolbarContext();
17919
+ const items = [
17920
+ paragraphItem,
17921
+ ...headingLevels.map((level) => headingItemsByLevel[level])
17922
+ ];
17516
17923
  const value = useEditorSelector((editor2) => {
17517
17924
  let initialNodeType = ParagraphPlugin.key;
17518
17925
  let allNodesMatchInitialNodeType = false;
@@ -17534,7 +17941,7 @@ function HeadingsMenu(props) {
17534
17941
  const editorState = useEditorState();
17535
17942
  const openState = useOpenState();
17536
17943
  const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
17537
- const selectedItem = items$1.find((item) => item.value === value) ?? defaultItem$1;
17944
+ const selectedItem = getHeadingItem(value) ?? paragraphItem;
17538
17945
  const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
17539
17946
  return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
17540
17947
  ToolbarButton,
@@ -17563,7 +17970,7 @@ function HeadingsMenu(props) {
17563
17970
  },
17564
17971
  value
17565
17972
  },
17566
- items$1.filter((item) => {
17973
+ items.filter((item) => {
17567
17974
  if (userInTable) {
17568
17975
  return !unsupportedItemsInTable.has(item.label);
17569
17976
  }
@@ -18239,27 +18646,27 @@ function FixedToolbarButtons() {
18239
18646
  const [itemsShown, setItemsShown] = React__default.useState(11);
18240
18647
  const { overrides, templates } = useToolbarContext();
18241
18648
  const showEmbedButton = templates.length > 0;
18242
- let items2 = [];
18649
+ let items = [];
18243
18650
  if (Array.isArray(overrides)) {
18244
- items2 = overrides === void 0 ? Object.values(toolbarItems) : overrides.map((item) => toolbarItems[item]).filter((item) => item !== void 0);
18651
+ items = overrides === void 0 ? Object.values(toolbarItems) : overrides.map((item) => toolbarItems[item]).filter((item) => item !== void 0);
18245
18652
  } else {
18246
- items2 = (overrides == null ? void 0 : overrides.toolbar) === void 0 ? Object.values(toolbarItems) : overrides.toolbar.map((item) => toolbarItems[item]).filter((item) => item !== void 0);
18653
+ items = (overrides == null ? void 0 : overrides.toolbar) === void 0 ? Object.values(toolbarItems) : overrides.toolbar.map((item) => toolbarItems[item]).filter((item) => item !== void 0);
18247
18654
  }
18248
18655
  if (!showEmbedButton) {
18249
- items2 = items2.filter((item) => item.label !== toolbarItems.embed.label);
18656
+ items = items.filter((item) => item.label !== toolbarItems.embed.label);
18250
18657
  }
18251
18658
  const editorState = useEditorState();
18252
18659
  const userInTable = helpers.isNodeActive(editorState, TablePlugin);
18253
18660
  const userInCodeBlock = helpers.isNodeActive(editorState, CodeBlockPlugin);
18254
18661
  useResize(toolbarRef, (entry) => {
18255
18662
  const width = entry.target.getBoundingClientRect().width - 8;
18256
- const headingButton = items2.find((item) => item.label === HEADING_LABEL);
18663
+ const headingButton = items.find((item) => item.label === HEADING_LABEL);
18257
18664
  const headingWidth = headingButton ? (
18258
18665
  //some discrepancy here between the md breakpoint here and in practice, but it works
18259
18666
  headingButton.width(width > CONTAINER_MD_BREAKPOINT - 9)
18260
18667
  ) : 0;
18261
18668
  const availableWidth = width - headingWidth;
18262
- const { itemFitCount } = items2.reduce(
18669
+ const { itemFitCount } = items.reduce(
18263
18670
  (acc, item) => {
18264
18671
  if (item.label !== HEADING_LABEL && acc.totalItemsWidth + item.width() <= availableWidth) {
18265
18672
  return {
@@ -18291,7 +18698,7 @@ function FixedToolbarButtons() {
18291
18698
  transform: "translateX(calc(-1px))"
18292
18699
  }
18293
18700
  },
18294
- /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, items2.slice(0, items2.length > itemsShown ? itemsShown - 1 : itemsShown).map((item) => /* @__PURE__ */ React__default.createElement(
18701
+ /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, items.slice(0, items.length > itemsShown ? itemsShown - 1 : itemsShown).map((item) => /* @__PURE__ */ React__default.createElement(
18295
18702
  "div",
18296
18703
  {
18297
18704
  className: cn(
@@ -18301,7 +18708,7 @@ function FixedToolbarButtons() {
18301
18708
  key: item.label
18302
18709
  },
18303
18710
  item.Component
18304
- )), items2.length > itemsShown && /* @__PURE__ */ React__default.createElement("div", { className: "w-fit ml-auto" }, /* @__PURE__ */ React__default.createElement(OverflowMenu, null, items2.slice(itemsShown - 1).flatMap((c2) => /* @__PURE__ */ React__default.createElement(
18711
+ )), items.length > itemsShown && /* @__PURE__ */ React__default.createElement("div", { className: "w-fit ml-auto" }, /* @__PURE__ */ React__default.createElement(OverflowMenu, null, items.slice(itemsShown - 1).flatMap((c2) => /* @__PURE__ */ React__default.createElement(
18305
18712
  "div",
18306
18713
  {
18307
18714
  className: cn(
@@ -18789,52 +19196,12 @@ const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
18789
19196
  children
18790
19197
  ));
18791
19198
  });
18792
- const items = [
18793
- {
18794
- description: "Paragraph",
18795
- icon: Icons.paragraph,
18796
- label: "Paragraph",
18797
- value: ParagraphPlugin.key
18798
- },
18799
- {
18800
- description: "Heading 1",
18801
- icon: Icons.h1,
18802
- label: "Heading 1",
18803
- value: HEADING_KEYS.h1
18804
- },
18805
- {
18806
- description: "Heading 2",
18807
- icon: Icons.h2,
18808
- label: "Heading 2",
18809
- value: HEADING_KEYS.h2
18810
- },
18811
- {
18812
- description: "Heading 3",
18813
- icon: Icons.h3,
18814
- label: "Heading 3",
18815
- value: HEADING_KEYS.h3
18816
- },
18817
- {
18818
- description: "Heading 4",
18819
- icon: Icons.h4,
18820
- label: "Heading 4",
18821
- value: HEADING_KEYS.h4
18822
- },
18823
- {
18824
- description: "Heading 5",
18825
- icon: Icons.h5,
18826
- label: "Heading 5",
18827
- value: HEADING_KEYS.h5
18828
- },
18829
- {
18830
- description: "Heading 6",
18831
- icon: Icons.h6,
18832
- label: "Heading 6",
18833
- value: HEADING_KEYS.h6
18834
- }
18835
- ];
18836
- const defaultItem = items.find((item) => item.value === ParagraphPlugin.key);
18837
19199
  function TurnIntoDropdownMenu(props) {
19200
+ const { headingLevels } = useToolbarContext();
19201
+ const items = [
19202
+ paragraphItem,
19203
+ ...headingLevels.map((level) => headingItemsByLevel[level])
19204
+ ];
18838
19205
  const value = useEditorSelector((editor2) => {
18839
19206
  let initialNodeType = ParagraphPlugin.key;
18840
19207
  let allNodesMatchInitialNodeType = false;
@@ -18854,7 +19221,7 @@ function TurnIntoDropdownMenu(props) {
18854
19221
  }, []);
18855
19222
  const editor = useEditorRef();
18856
19223
  const openState = useOpenState();
18857
- const selectedItem = items.find((item) => item.value === value) ?? defaultItem;
19224
+ const selectedItem = getHeadingItem(value) ?? paragraphItem;
18858
19225
  const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
18859
19226
  const editorState = useEditorState();
18860
19227
  const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
@@ -61927,9 +62294,9 @@ function php(hljs) {
61927
62294
  "static",
61928
62295
  "stdClass"
61929
62296
  ];
61930
- const dualCase = (items2) => {
62297
+ const dualCase = (items) => {
61931
62298
  const result = [];
61932
- items2.forEach((item) => {
62299
+ items.forEach((item) => {
61933
62300
  result.push(item);
61934
62301
  if (item.toLowerCase() === item) {
61935
62302
  result.push(item.toUpperCase());
@@ -61944,8 +62311,8 @@ function php(hljs) {
61944
62311
  literal: dualCase(LITERALS2),
61945
62312
  built_in: BUILT_INS2
61946
62313
  };
61947
- const normalizeKeywords = (items2) => {
61948
- return items2.map((item) => {
62314
+ const normalizeKeywords = (items) => {
62315
+ return items.map((item) => {
61949
62316
  return item.replace(/\|\d+$/, "");
61950
62317
  });
61951
62318
  };
@@ -69713,7 +70080,9 @@ const ClearHighlightOnEnterPlugin = createSlatePlugin({
69713
70080
  }
69714
70081
  }
69715
70082
  }));
69716
- const editorPlugins = [
70083
+ const createEditorPlugins = ({
70084
+ headingLevels
70085
+ } = {}) => [
69717
70086
  createMdxBlockPlugin,
69718
70087
  createMdxInlinePlugin,
69719
70088
  createImgPlugin,
@@ -69737,9 +70106,9 @@ const editorPlugins = [
69737
70106
  NodeIdPlugin,
69738
70107
  TablePlugin,
69739
70108
  SlashPlugin,
69740
- // This lets users keep typing after end of marks like headings or quotes
70109
+ // Keeps a blank paragraph at the end of the editor so users can keep
70110
+ // typing after a heading, quote, or other terminal block.
69741
70111
  TrailingBlockPlugin,
69742
- //makes sure there's always a blank paragraph at the end of the editor.
69743
70112
  createBreakPlugin,
69744
70113
  FloatingToolbarPlugin,
69745
70114
  AutoformatPlugin.configure({
@@ -69747,7 +70116,7 @@ const editorPlugins = [
69747
70116
  enableUndoOnDelete: true,
69748
70117
  rules: [
69749
70118
  ...autoformatMarks,
69750
- ...autoformatBlocks,
70119
+ ...getAutoformatBlocks(headingLevels),
69751
70120
  ...autoformatLists,
69752
70121
  ...autoformatSmartQuotes,
69753
70122
  ...autoformatPunctuation,
@@ -69757,6 +70126,7 @@ const editorPlugins = [
69757
70126
  ].map(
69758
70127
  (rule) => ({
69759
70128
  ...rule,
70129
+ // Suppress autoformat inside code blocks so e.g. `# ` stays literal.
69760
70130
  query: (editor) => !editor.api.some({
69761
70131
  match: { type: editor.getType(CodeBlockPlugin) }
69762
70132
  })
@@ -69764,29 +70134,21 @@ const editorPlugins = [
69764
70134
  )
69765
70135
  }
69766
70136
  }),
69767
- // ExitBreakPlugin lets users "break out" of a block (like a heading)
70137
+ // Lets users "break out" of a block (e.g. a heading) with Enter.
69768
70138
  ExitBreakPlugin.configure({
69769
70139
  options: {
69770
70140
  rules: [
69771
- {
69772
- hotkey: "mod+enter"
69773
- },
69774
- {
69775
- hotkey: "mod+shift+enter",
69776
- before: true
69777
- },
70141
+ { hotkey: "mod+enter" },
70142
+ { hotkey: "mod+shift+enter", before: true },
69778
70143
  {
69779
70144
  hotkey: "enter",
69780
- query: {
69781
- start: true,
69782
- end: true,
69783
- allow: HEADING_LEVELS
69784
- }
70145
+ query: { start: true, end: true, allow: HEADING_LEVELS }
69785
70146
  }
69786
70147
  ]
69787
70148
  }
69788
70149
  }),
69789
- // ResetNodePlugin lets users turn a heading back into a paragraph by pressing Enter (when empty) or Backspace (at the start).
70150
+ // Lets users turn a heading back into a paragraph by pressing Enter on
70151
+ // an empty heading or Backspace at the start of one.
69790
70152
  ResetNodePlugin.configure({
69791
70153
  options: {
69792
70154
  rules: [
@@ -69798,9 +70160,7 @@ const editorPlugins = [
69798
70160
  {
69799
70161
  ...resetBlockTypesCommonRule,
69800
70162
  hotkey: "Backspace",
69801
- predicate: (editor) => {
69802
- return editor.api.isAt({ start: true });
69803
- }
70163
+ predicate: (editor) => editor.api.isAt({ start: true })
69804
70164
  },
69805
70165
  {
69806
70166
  ...resetBlockTypesCodeBlockRule,
@@ -69812,10 +70172,11 @@ const editorPlugins = [
69812
70172
  hotkey: "Backspace",
69813
70173
  predicate: isSelectionAtCodeBlockStart
69814
70174
  },
69815
- // NOTE: Plate's ListPlugin usually handles resetting lists to paragraphs when pressing Backspace at the start of a list item.
69816
- // However, if the list is the first node in the editor, the default reset behavior may not fully unwrap the list item,
69817
- // which can leave an invalid structure (like a <li> inside a <p>).
69818
- // This rule uses `onReset: unwrapList` to ensure lists are always properly reset to paragraphs, even when they are the first node.
70175
+ // Plate's ListPlugin usually resets lists to paragraphs on Backspace
70176
+ // at the start of a list item, but when the list is the editor's
70177
+ // first node the default doesn't fully unwrap and we end up with an
70178
+ // invalid structure (e.g. <li> inside <p>). `onReset: unwrapList`
70179
+ // forces a clean reset in that case.
69819
70180
  {
69820
70181
  types: [BulletedListPlugin.key, NumberedListPlugin.key],
69821
70182
  defaultType: ParagraphPlugin.key,
@@ -69832,21 +70193,19 @@ const editorPlugins = [
69832
70193
  { hotkey: "shift+enter" },
69833
70194
  {
69834
70195
  hotkey: "enter",
69835
- query: {
69836
- allow: [CodeBlockPlugin.key, BlockquotePlugin.key]
69837
- }
70196
+ query: { allow: [CodeBlockPlugin.key, BlockquotePlugin.key] }
69838
70197
  }
69839
70198
  ]
69840
70199
  }
69841
70200
  })
69842
70201
  ];
69843
70202
  const RichEditor = ({ input, tinaForm, field }) => {
69844
- var _a;
70203
+ var _a, _b;
69845
70204
  const initialValue = React__default.useMemo(() => {
69846
- var _a2, _b, _c;
70205
+ var _a2, _b2, _c;
69847
70206
  if (((_a2 = field == null ? void 0 : field.parser) == null ? void 0 : _a2.type) === "slatejson") {
69848
70207
  return input.value.children;
69849
- } else if ((_c = (_b = input.value) == null ? void 0 : _b.children) == null ? void 0 : _c.length) {
70208
+ } else if ((_c = (_b2 = input.value) == null ? void 0 : _b2.children) == null ? void 0 : _c.length) {
69850
70209
  const normalized = input.value.children.map(helpers.normalize);
69851
70210
  return normalized;
69852
70211
  } else {
@@ -69854,7 +70213,16 @@ const RichEditor = ({ input, tinaForm, field }) => {
69854
70213
  }
69855
70214
  }, []);
69856
70215
  const showFloatingToolbar = ((_a = field == null ? void 0 : field.overrides) == null ? void 0 : _a.showFloatingToolbar) !== false;
69857
- const plugins = showFloatingToolbar ? editorPlugins : editorPlugins.filter((plugin) => plugin.key !== "floating-toolbar");
70216
+ const builtPlugins = React__default.useMemo(
70217
+ () => {
70218
+ var _a2;
70219
+ return createEditorPlugins({
70220
+ headingLevels: (_a2 = field == null ? void 0 : field.overrides) == null ? void 0 : _a2.headingLevels
70221
+ });
70222
+ },
70223
+ [(_b = field == null ? void 0 : field.overrides) == null ? void 0 : _b.headingLevels]
70224
+ );
70225
+ const plugins = showFloatingToolbar ? builtPlugins : builtPlugins.filter((plugin) => plugin.key !== "floating-toolbar");
69858
70226
  const editor = useCreateEditor({
69859
70227
  plugins: [...plugins],
69860
70228
  value: initialValue,
@@ -69896,8 +70264,9 @@ const RichEditor = ({ input, tinaForm, field }) => {
69896
70264
  templates: field.templates,
69897
70265
  overrides: (field == null ? void 0 : field.toolbarOverride) ? field.toolbarOverride : field.overrides
69898
70266
  },
69899
- /* @__PURE__ */ React__default.createElement(FixedToolbar, null, /* @__PURE__ */ React__default.createElement(FixedToolbarButtons, null))
69900
- ), /* @__PURE__ */ React__default.createElement(Editor$1, null)))
70267
+ /* @__PURE__ */ React__default.createElement(FixedToolbar, null, /* @__PURE__ */ React__default.createElement(FixedToolbarButtons, null)),
70268
+ /* @__PURE__ */ React__default.createElement(Editor$1, null)
70269
+ )))
69901
70270
  ));
69902
70271
  };
69903
70272
  const MdxFieldPlugin = {
@@ -69999,6 +70368,191 @@ function useLocalStorage(key, initialValue) {
69999
70368
  };
70000
70369
  return [storedValue, setValue];
70001
70370
  }
70371
+ const MediaWorkflowOverlay = () => {
70372
+ const cms = useCMS$1();
70373
+ const { setCurrentBranch } = useBranchData();
70374
+ const [state, setState] = React.useState({ phase: "idle" });
70375
+ const preflightAbortRef = React.useRef(null);
70376
+ const abortPreflight = React.useCallback(() => {
70377
+ var _a;
70378
+ (_a = preflightAbortRef.current) == null ? void 0 : _a.abort();
70379
+ preflightAbortRef.current = null;
70380
+ }, []);
70381
+ React.useEffect(() => {
70382
+ const offConfirm = cms.events.subscribe(
70383
+ "media:workflow:confirm-branch",
70384
+ (event) => {
70385
+ abortPreflight();
70386
+ setState({
70387
+ phase: "confirming",
70388
+ branchName: event.branchName,
70389
+ baseBranch: event.baseBranch,
70390
+ onConfirm: event.onConfirm,
70391
+ onCancel: event.onCancel,
70392
+ onSaveToProtectedBranch: event.onSaveToProtectedBranch
70393
+ });
70394
+ }
70395
+ );
70396
+ const offStart = cms.events.subscribe("media:workflow:start", () => {
70397
+ setState({ phase: "executing", step: 1, elapsed: 0 });
70398
+ });
70399
+ const offStep = cms.events.subscribe(
70400
+ "media:workflow:step",
70401
+ (event) => {
70402
+ setState(
70403
+ (prev) => prev.phase === "executing" ? { ...prev, step: event.step } : { phase: "executing", step: event.step, elapsed: 0 }
70404
+ );
70405
+ }
70406
+ );
70407
+ const offComplete = cms.events.subscribe("media:workflow:complete", (event) => {
70408
+ setCurrentBranch(event.branchName);
70409
+ });
70410
+ const offError = cms.events.subscribe(
70411
+ "media:workflow:error",
70412
+ (event) => {
70413
+ setState({ phase: "error", message: event.message });
70414
+ }
70415
+ );
70416
+ const offFinish = cms.events.subscribe("media:workflow:finish", () => {
70417
+ setState({ phase: "idle" });
70418
+ });
70419
+ return () => {
70420
+ offConfirm();
70421
+ offStart();
70422
+ offStep();
70423
+ offComplete();
70424
+ offError();
70425
+ offFinish();
70426
+ abortPreflight();
70427
+ };
70428
+ }, [abortPreflight, cms, setCurrentBranch]);
70429
+ React.useEffect(() => {
70430
+ if (state.phase !== "executing")
70431
+ return;
70432
+ const interval = setInterval(() => {
70433
+ setState(
70434
+ (prev) => prev.phase === "executing" ? { ...prev, elapsed: prev.elapsed + 1 } : prev
70435
+ );
70436
+ }, 1e3);
70437
+ return () => clearInterval(interval);
70438
+ }, [state.phase]);
70439
+ const handleCreateBranch = async () => {
70440
+ if (state.phase !== "confirming")
70441
+ return;
70442
+ const confirmState = state;
70443
+ const branchName = confirmState.branchName;
70444
+ const targetBranch = `tina/${branchName}`;
70445
+ abortPreflight();
70446
+ const abortController = new AbortController();
70447
+ preflightAbortRef.current = abortController;
70448
+ setState({
70449
+ ...confirmState,
70450
+ isChecking: true,
70451
+ errorMessage: ""
70452
+ });
70453
+ const baseBranchExists = await checkBaseBranchExists(
70454
+ cms.api.tina,
70455
+ confirmState.baseBranch,
70456
+ "media workflow",
70457
+ abortController.signal
70458
+ );
70459
+ if (abortController.signal.aborted)
70460
+ return;
70461
+ if (!baseBranchExists) {
70462
+ if (preflightAbortRef.current === abortController) {
70463
+ preflightAbortRef.current = null;
70464
+ }
70465
+ setState({
70466
+ ...confirmState,
70467
+ branchName,
70468
+ isChecking: false,
70469
+ errorMessage: `The branch ${confirmState.baseBranch} no longer exists. It may have been merged or deleted. Your changes cannot be pushed to it.`
70470
+ });
70471
+ return;
70472
+ }
70473
+ const targetBranchExists = await checkTargetBranchExists(
70474
+ cms.api.tina,
70475
+ targetBranch,
70476
+ "media workflow",
70477
+ abortController.signal
70478
+ );
70479
+ if (abortController.signal.aborted)
70480
+ return;
70481
+ if (targetBranchExists) {
70482
+ if (preflightAbortRef.current === abortController) {
70483
+ preflightAbortRef.current = null;
70484
+ }
70485
+ setState({
70486
+ ...confirmState,
70487
+ branchName,
70488
+ isChecking: false,
70489
+ errorMessage: TARGET_BRANCH_EXISTS_ERROR
70490
+ });
70491
+ return;
70492
+ }
70493
+ try {
70494
+ if (preflightAbortRef.current === abortController) {
70495
+ preflightAbortRef.current = null;
70496
+ }
70497
+ setState({ phase: "executing", step: 1, elapsed: 0 });
70498
+ await confirmState.onConfirm(targetBranch);
70499
+ } catch (e) {
70500
+ console.error(e);
70501
+ setState({
70502
+ ...confirmState,
70503
+ branchName,
70504
+ isChecking: false,
70505
+ errorMessage: getEditorialWorkflowErrorMessage(e)
70506
+ });
70507
+ }
70508
+ };
70509
+ if (state.phase === "idle")
70510
+ return null;
70511
+ if (state.phase === "confirming") {
70512
+ return /* @__PURE__ */ React.createElement(
70513
+ CreateBranchPromptModal,
70514
+ {
70515
+ branchName: state.branchName,
70516
+ close: () => {
70517
+ abortPreflight();
70518
+ state.onCancel();
70519
+ setState({ phase: "idle" });
70520
+ },
70521
+ disabled: state.branchName === "" || state.isChecking,
70522
+ errorMessage: state.errorMessage,
70523
+ onBranchNameChange: (branchName) => {
70524
+ abortPreflight();
70525
+ setState(
70526
+ (prev) => prev.phase === "confirming" ? {
70527
+ ...prev,
70528
+ branchName,
70529
+ errorMessage: void 0,
70530
+ isChecking: false
70531
+ } : prev
70532
+ );
70533
+ },
70534
+ onCreateBranch: handleCreateBranch,
70535
+ onSaveToProtectedBranch: () => {
70536
+ abortPreflight();
70537
+ state.onSaveToProtectedBranch();
70538
+ setState({ phase: "idle" });
70539
+ }
70540
+ }
70541
+ );
70542
+ }
70543
+ if (state.phase === "executing") {
70544
+ return /* @__PURE__ */ React.createElement(
70545
+ EditorialWorkflowProgressModal,
70546
+ {
70547
+ title: "Save changes to new branch",
70548
+ currentStep: state.step,
70549
+ elapsedTime: state.elapsed
70550
+ }
70551
+ );
70552
+ }
70553
+ const dismissError = () => setState({ phase: "idle" });
70554
+ return /* @__PURE__ */ React.createElement(Modal, { className: "flex" }, /* @__PURE__ */ React.createElement(PopupModal, { className: "w-auto" }, /* @__PURE__ */ React.createElement(ModalHeader, { close: dismissError }, "Branch creation failed"), /* @__PURE__ */ React.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 text-red-700 py-2 px-3 bg-red-50 border border-red-200 rounded max-w-sm" }, /* @__PURE__ */ React.createElement(BiError, { className: "w-5 h-auto text-red-400 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm" }, /* @__PURE__ */ React.createElement("b", null, "Error:"), " ", state.message)))));
70555
+ };
70002
70556
  function asyncPoll(fn, pollInterval = 5 * 1e3, pollTimeout = 30 * 1e3) {
70003
70557
  const endTime = (/* @__PURE__ */ new Date()).getTime() + pollTimeout;
70004
70558
  let stop = false;
@@ -70558,7 +71112,7 @@ mutation addPendingDocumentMutation(
70558
71112
  }
70559
71113
  });
70560
71114
  if (!res.ok) {
70561
- let errorMessage = `There was an error creating a new branch. ${res.statusText}`;
71115
+ let errorMessage = `There was an error creating a pull request. ${res.statusText}`;
70562
71116
  if (res.status === 422) {
70563
71117
  errorMessage = `Please make sure you have made changes on ${branch} before creating a pull request.`;
70564
71118
  }
@@ -70567,7 +71121,7 @@ mutation addPendingDocumentMutation(
70567
71121
  const values = await res.json();
70568
71122
  return values;
70569
71123
  } catch (error2) {
70570
- console.error("There was an error creating a new branch.", error2);
71124
+ console.error("There was an error creating a pull request.", error2);
70571
71125
  throw error2;
70572
71126
  }
70573
71127
  }
@@ -70664,7 +71218,8 @@ mutation addPendingDocumentMutation(
70664
71218
  try {
70665
71219
  const url = `${this.contentApiBase}/github/${this.clientId}/list_branches`;
70666
71220
  const res = await this.authProvider.fetchWithToken(url, {
70667
- method: "GET"
71221
+ method: "GET",
71222
+ signal: args == null ? void 0 : args.signal
70668
71223
  });
70669
71224
  const branches = await res.json();
70670
71225
  const parsedBranches = await ListBranchResponse.parseAsync(branches);
@@ -70690,10 +71245,13 @@ mutation addPendingDocumentMutation(
70690
71245
  var _a;
70691
71246
  return this.usingEditorialWorkflow && ((_a = this.protectedBranches) == null ? void 0 : _a.includes(decodeURIComponent(this.branch)));
70692
71247
  }
70693
- async branchExists(branchName) {
71248
+ async branchExists(branchName, args) {
70694
71249
  if (this.isLocalMode)
70695
71250
  return true;
70696
- const branches = await this.listBranches({ includeIndexStatus: false });
71251
+ const branches = await this.listBranches({
71252
+ includeIndexStatus: false,
71253
+ signal: args == null ? void 0 : args.signal
71254
+ });
70697
71255
  return branches.some((b) => b.name === branchName);
70698
71256
  }
70699
71257
  async createBranch({ baseBranch, branchName }) {
@@ -70737,6 +71295,89 @@ mutation addPendingDocumentMutation(
70737
71295
  throw error2;
70738
71296
  }
70739
71297
  }
71298
+ async pollEditorialWorkflowStatus(requestId, onStatusUpdate) {
71299
+ const pollInterval = 5e3;
71300
+ const maxAttempts = 180;
71301
+ let attempts = 0;
71302
+ while (attempts < maxAttempts) {
71303
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
71304
+ attempts++;
71305
+ try {
71306
+ const statusUrl = `${this.contentApiBase}/editorial-workflow/${this.clientId}/status/${requestId}`;
71307
+ const statusResponse = await this.authProvider.fetchWithToken(statusUrl);
71308
+ const statusResponseBody = await statusResponse.json();
71309
+ onStatusUpdate == null ? void 0 : onStatusUpdate({
71310
+ status: statusResponseBody.status,
71311
+ message: statusResponseBody.message || `Status: ${statusResponseBody.status}`
71312
+ });
71313
+ if (statusResponseBody.status === EDITORIAL_WORKFLOW_STATUS.ERROR || statusResponse.status === 500) {
71314
+ if (statusResponseBody.pullRequestUrl) {
71315
+ return {
71316
+ branchName: statusResponseBody.branchName,
71317
+ pullRequestUrl: statusResponseBody.pullRequestUrl,
71318
+ warning: statusResponseBody.message
71319
+ };
71320
+ }
71321
+ const error2 = new Error(
71322
+ statusResponseBody.message || "Editorial workflow failed"
71323
+ );
71324
+ error2.errorCode = statusResponseBody.errorCode || "WORKFLOW_FAILED";
71325
+ throw error2;
71326
+ }
71327
+ if (statusResponse.status === 200) {
71328
+ return {
71329
+ branchName: statusResponseBody.branchName,
71330
+ pullRequestUrl: statusResponseBody.pullRequestUrl
71331
+ };
71332
+ }
71333
+ if (statusResponse.status !== 202) {
71334
+ const error2 = new Error(
71335
+ statusResponseBody.message || `Failed to check workflow status: ${statusResponse.statusText}`
71336
+ );
71337
+ error2.errorCode = "WORKFLOW_STATUS_FAILED";
71338
+ throw error2;
71339
+ }
71340
+ } catch (error2) {
71341
+ if (error2.errorCode) {
71342
+ throw error2;
71343
+ }
71344
+ console.warn(
71345
+ `Editorial workflow status poll failed (attempt ${attempts}/${maxAttempts}), retrying...`,
71346
+ error2
71347
+ );
71348
+ }
71349
+ }
71350
+ const timeoutMinutes = Math.round(maxAttempts * pollInterval / 6e4);
71351
+ throw new Error(
71352
+ `Editorial workflow timed out after ${timeoutMinutes} minutes. It may still be completing in the background — please wait before retrying.`
71353
+ );
71354
+ }
71355
+ toEditorialWorkflowError(responseBody, fallbackMessage) {
71356
+ const error2 = new Error(
71357
+ (responseBody == null ? void 0 : responseBody.message) || fallbackMessage
71358
+ );
71359
+ if (responseBody == null ? void 0 : responseBody.errorCode) {
71360
+ error2.errorCode = responseBody.errorCode;
71361
+ }
71362
+ if (responseBody == null ? void 0 : responseBody.conflictingBranch) {
71363
+ error2.conflictingBranch = responseBody.conflictingBranch;
71364
+ }
71365
+ return error2;
71366
+ }
71367
+ async postEditorialWorkflow(url, body, errorFallback) {
71368
+ const res = await this.authProvider.fetchWithToken(url, {
71369
+ method: "POST",
71370
+ body: JSON.stringify(body),
71371
+ headers: {
71372
+ "Content-Type": "application/json"
71373
+ }
71374
+ });
71375
+ const responseBody = await res.json();
71376
+ if (!res.ok) {
71377
+ throw this.toEditorialWorkflowError(responseBody, errorFallback);
71378
+ }
71379
+ return responseBody;
71380
+ }
70740
71381
  /**
70741
71382
  * Initiate and poll for the results of an editorial workflow operation
70742
71383
  *
@@ -70746,32 +71387,16 @@ mutation addPendingDocumentMutation(
70746
71387
  async executeEditorialWorkflow(options) {
70747
71388
  const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}`;
70748
71389
  try {
70749
- const res = await this.authProvider.fetchWithToken(url, {
70750
- method: "POST",
70751
- body: JSON.stringify({
71390
+ const responseBody = await this.postEditorialWorkflow(
71391
+ url,
71392
+ {
70752
71393
  branchName: options.branchName,
70753
71394
  baseBranch: options.baseBranch,
70754
71395
  prTitle: options.prTitle,
70755
71396
  graphQLContentOp: options.graphQLContentOp
70756
- }),
70757
- headers: {
70758
- "Content-Type": "application/json"
70759
- }
70760
- });
70761
- const responseBody = await res.json();
70762
- if (!res.ok) {
70763
- console.error("There was an error starting editorial workflow.");
70764
- const error2 = new Error(
70765
- (responseBody == null ? void 0 : responseBody.message) || "Failed to start editorial workflow"
70766
- );
70767
- if (responseBody == null ? void 0 : responseBody.errorCode) {
70768
- error2.errorCode = responseBody.errorCode;
70769
- }
70770
- if (responseBody == null ? void 0 : responseBody.conflictingBranch) {
70771
- error2.conflictingBranch = responseBody.conflictingBranch;
70772
- }
70773
- throw error2;
70774
- }
71397
+ },
71398
+ "Failed to start editorial workflow"
71399
+ );
70775
71400
  const requestId = responseBody.requestId;
70776
71401
  if (!requestId) {
70777
71402
  return responseBody;
@@ -70782,60 +71407,10 @@ mutation addPendingDocumentMutation(
70782
71407
  message: "Workflow queued, starting..."
70783
71408
  });
70784
71409
  }
70785
- const maxAttempts = 60;
70786
- const pollInterval = 5e3;
70787
- let attempts = 0;
70788
- while (attempts < maxAttempts) {
70789
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
70790
- attempts++;
70791
- try {
70792
- const statusUrl = `${this.contentApiBase}/editorial-workflow/${this.clientId}/status/${requestId}`;
70793
- const statusResponse = await this.authProvider.fetchWithToken(statusUrl);
70794
- const statusResponseBody = await statusResponse.json();
70795
- if (options.onStatusUpdate) {
70796
- options.onStatusUpdate({
70797
- status: statusResponseBody.status,
70798
- message: statusResponseBody.message || `Status: ${statusResponseBody.status}`
70799
- });
70800
- }
70801
- if (statusResponseBody.status === EDITORIAL_WORKFLOW_STATUS.ERROR || statusResponse.status === 500) {
70802
- if (statusResponseBody.pullRequestUrl) {
70803
- return {
70804
- branchName: statusResponseBody.branchName,
70805
- pullRequestUrl: statusResponseBody.pullRequestUrl,
70806
- warning: statusResponseBody.message
70807
- };
70808
- }
70809
- const error2 = new Error(
70810
- statusResponseBody.message || "Editorial workflow failed"
70811
- );
70812
- error2.errorCode = statusResponseBody.errorCode || "WORKFLOW_FAILED";
70813
- throw error2;
70814
- }
70815
- if (statusResponse.status === 200) {
70816
- return {
70817
- branchName: statusResponseBody.branchName,
70818
- pullRequestUrl: statusResponseBody.pullRequestUrl
70819
- };
70820
- }
70821
- if (statusResponse.status !== 202) {
70822
- const error2 = new Error(
70823
- statusResponseBody.message || `Failed to check workflow status: ${statusResponse.statusText}`
70824
- );
70825
- error2.errorCode = "WORKFLOW_STATUS_FAILED";
70826
- throw error2;
70827
- }
70828
- } catch (error2) {
70829
- if (error2.errorCode) {
70830
- throw error2;
70831
- }
70832
- console.warn(
70833
- `Editorial workflow status poll failed (attempt ${attempts}/${maxAttempts}), retrying...`,
70834
- error2
70835
- );
70836
- }
70837
- }
70838
- throw new Error("Editorial workflow timed out after 5 minutes");
71410
+ return await this.pollEditorialWorkflowStatus(
71411
+ requestId,
71412
+ options.onStatusUpdate
71413
+ );
70839
71414
  } catch (error2) {
70840
71415
  console.error(
70841
71416
  "There was an error with editorial workflow operation.",
@@ -70844,6 +71419,17 @@ mutation addPendingDocumentMutation(
70844
71419
  throw error2;
70845
71420
  }
70846
71421
  }
71422
+ async startMediaEditorialWorkflow(options) {
71423
+ const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}/media`;
71424
+ return await this.postEditorialWorkflow(
71425
+ url,
71426
+ options,
71427
+ "Failed to start media editorial workflow"
71428
+ );
71429
+ }
71430
+ async waitForEditorialWorkflowStatus(requestId, onStatusUpdate) {
71431
+ return await this.pollEditorialWorkflowStatus(requestId, onStatusUpdate);
71432
+ }
70847
71433
  }
70848
71434
  const DEFAULT_LOCAL_TINA_GQL_SERVER_URL = "http://localhost:4001/graphql";
70849
71435
  class LocalClient extends Client {
@@ -71397,7 +71983,7 @@ const TinaCloudProvider = (props) => {
71397
71983
  }, []);
71398
71984
  React__default.useEffect(() => {
71399
71985
  const setupEditorialWorkflow = () => {
71400
- client.getProject().then((project) => {
71986
+ client.getProject().then(async (project) => {
71401
71987
  var _a2;
71402
71988
  if ((_a2 = project == null ? void 0 : project.features) == null ? void 0 : _a2.includes("editorial-workflow")) {
71403
71989
  cms.flags.set("branch-switcher", true);
@@ -71427,7 +72013,7 @@ const TinaCloudProvider = (props) => {
71427
72013
  setCurrentBranch(b);
71428
72014
  }
71429
72015
  },
71430
- /* @__PURE__ */ React__default.createElement(TinaProvider, { cms }, /* @__PURE__ */ React__default.createElement(AuthWallInner, { ...props, cms }))
72016
+ /* @__PURE__ */ React__default.createElement(TinaProvider, { cms }, /* @__PURE__ */ React__default.createElement(MediaWorkflowOverlay, null), /* @__PURE__ */ React__default.createElement(AuthWallInner, { ...props, cms }))
71431
72017
  ));
71432
72018
  };
71433
72019
  const TinaCloudAuthWall = TinaCloudProvider;
@@ -73574,7 +74160,7 @@ const RenderForm$1 = ({
73574
74160
  };
73575
74161
  }
73576
74162
  }
73577
- const defaultItem2 = customDefaults || // @ts-ignore internal types aren't up to date
74163
+ const defaultItem = customDefaults || // @ts-ignore internal types aren't up to date
73578
74164
  ((_d = template.ui) == null ? void 0 : _d.defaultItem) || // @ts-ignore
73579
74165
  (template == null ? void 0 : template.defaultItem) || {};
73580
74166
  const fileReadOnly = (_f = (_e = schemaCollection == null ? void 0 : schemaCollection.ui) == null ? void 0 : _e.filename) == null ? void 0 : _f.readonly;
@@ -73630,7 +74216,7 @@ const RenderForm$1 = ({
73630
74216
  const folderName = folder.fullyQualifiedName ? folder.name : "";
73631
74217
  return new Form({
73632
74218
  crudType: "create",
73633
- initialValues: typeof defaultItem2 === "function" ? { ...defaultItem2(), _template: templateName } : { ...defaultItem2, _template: templateName },
74219
+ initialValues: typeof defaultItem === "function" ? { ...defaultItem(), _template: templateName } : { ...defaultItem, _template: templateName },
73634
74220
  extraSubscribeValues: { active: true, submitting: true, touched: true },
73635
74221
  onChange: (values) => {
73636
74222
  var _a3, _b3;
@@ -74462,6 +75048,7 @@ export {
74462
75048
  ColorPicker,
74463
75049
  CreateBranchModal,
74464
75050
  CreateBranchModel,
75051
+ CreateBranchPromptModal,
74465
75052
  CursorPaginator,
74466
75053
  DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
74467
75054
  DEFAULT_MEDIA_UPLOAD_TYPES,
@@ -74540,6 +75127,7 @@ export {
74540
75127
  MediaIcon,
74541
75128
  MediaListError,
74542
75129
  MediaManager$1 as MediaManager,
75130
+ MediaWorkflowOverlay,
74543
75131
  Message,
74544
75132
  Modal,
74545
75133
  ModalActions,
@@ -74639,6 +75227,7 @@ export {
74639
75227
  passwordFieldClasses,
74640
75228
  resolveField,
74641
75229
  safeAssertShape,
75230
+ sanitizeFilename,
74642
75231
  selectFieldClasses,
74643
75232
  sortBranchListFn,
74644
75233
  staticRequest,