tinacms 3.8.4 → 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.
package/dist/index.js CHANGED
@@ -58,10 +58,10 @@ import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, close
58
58
  import { sortableKeyboardCoordinates, useSortable, SortableContext, verticalListSortingStrategy, defaultAnimateLayoutChanges } from "@dnd-kit/sortable";
59
59
  import { CSS } from "@dnd-kit/utilities";
60
60
  import { FaSpinner, FaCircle, FaFolder, FaFile, FaLock, FaUnlock } from "react-icons/fa";
61
+ import { AiFillWarning, AiOutlineLoading } from "react-icons/ai";
61
62
  import { buildSchema, print, getIntrospectionQuery, buildClientSchema, parse as parse$3 } from "graphql";
62
63
  import { diff as diff$1 } from "@graphql-inspector/core";
63
64
  import { processDocumentForIndexing, queryToSearchIndexQuery, optionsToSearchIndexOptions, parseSearchIndexResponse } from "@tinacms/search/index-client";
64
- import { AiFillWarning, AiOutlineLoading } from "react-icons/ai";
65
65
  import { HexColorPicker } from "react-colorful";
66
66
  import { MdKeyboardArrowDown, MdOutlineClear, MdArrowForward, MdAccessTime, MdOutlineDataSaverOff, MdCheckCircle, MdWifiOff, MdOutlineSettings, MdOutlinePhotoLibrary, MdVpnKey, MdImage, MdOutlineCloud, MdWarning, MdInfo, MdError, MdSyncProblem, MdOutlinePerson, MdOutlineHelpOutline } from "react-icons/md";
67
67
  import * as dropzone from "react-dropzone";
@@ -9306,12 +9306,23 @@ const PullRequestCell = ({
9306
9306
  }
9307
9307
  return null;
9308
9308
  };
9309
- 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 ";
9310
9309
  function formatBranchName(str) {
9311
- const pattern = /[^/\w-]+/g;
9312
- const formattedStr = str.replace(pattern, "-");
9313
- 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;
9314
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 ";
9315
9326
  const BranchSwitcher = (props) => {
9316
9327
  const cms = useCMS$1();
9317
9328
  const usingEditorialWorkflow = cms.api.tina.usingEditorialWorkflow;
@@ -10014,9 +10025,28 @@ class EventBus {
10014
10025
  }
10015
10026
  dispatch(event) {
10016
10027
  if (!this.listeners)
10017
- return;
10028
+ return false;
10018
10029
  const listenerSnapshot = Array.from(this.listeners.values());
10019
- 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;
10020
10050
  }
10021
10051
  }
10022
10052
  class Listener {
@@ -10031,6 +10061,16 @@ class Listener {
10031
10061
  }
10032
10062
  return false;
10033
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
+ }
10034
10074
  watchesEvent(currentEvent) {
10035
10075
  if (this.eventPattern === "*")
10036
10076
  return true;
@@ -10047,6 +10087,63 @@ class Listener {
10047
10087
  return !ignoresEvent;
10048
10088
  }
10049
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
+ };
10050
10147
  const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
10051
10148
  class DummyMediaStore {
10052
10149
  constructor() {
@@ -10084,6 +10181,9 @@ class TinaMediaStore {
10084
10181
  __publicField(this, "url");
10085
10182
  __publicField(this, "staticMedia");
10086
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);
10087
10187
  __publicField(this, "accept", DEFAULT_MEDIA_UPLOAD_TYPES);
10088
10188
  // allow up to 100MB uploads
10089
10189
  __publicField(this, "maxSize", 100 * 1024 * 1024);
@@ -10123,8 +10223,8 @@ class TinaMediaStore {
10123
10223
  return await this.api.authProvider.isAuthenticated();
10124
10224
  }
10125
10225
  /**
10126
- * Returns the current branch as a single-encoded query-param value, or
10127
- * 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.
10128
10228
  *
10129
10229
  * `this.api.branch` is already URL-encoded by `Client.setBranch()`, so we
10130
10230
  * decode then re-encode here to defend against double-encoding when this
@@ -10137,6 +10237,9 @@ class TinaMediaStore {
10137
10237
  * assets-api (which would route the call to a non-existent staging path).
10138
10238
  */
10139
10239
  encodedBranchParam() {
10240
+ if (this.workflowBranchOverride) {
10241
+ return encodeURIComponent(this.workflowBranchOverride);
10242
+ }
10140
10243
  if (!this.api.branch)
10141
10244
  return "";
10142
10245
  const decoded = decodeURIComponent(this.api.branch);
@@ -10144,67 +10247,297 @@ class TinaMediaStore {
10144
10247
  return "";
10145
10248
  return encodeURIComponent(decoded);
10146
10249
  }
10147
- async persist_cloud(media) {
10148
- if (!await this.isAuthenticated()) {
10149
- 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);
10150
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() {
10151
10284
  const encodedBranch = this.encodedBranchParam();
10152
- const branchQuery = encodedBranch ? `?branch=${encodedBranch}` : "";
10153
- for (const item of media) {
10154
- let directory = item.directory;
10155
- if (directory == null ? void 0 : directory.endsWith("/")) {
10156
- directory = directory.substr(0, directory.length - 1);
10157
- }
10158
- const safeName = sanitizeFilename(item.file.name);
10159
- const path = `${directory && directory !== "/" ? `${directory}/${safeName}` : safeName}`;
10160
- const res = await this.api.authProvider.fetchWithToken(
10161
- `${this.url}/upload_url/${path}${branchQuery}`,
10162
- { 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)."
10163
10291
  );
10164
- if (res.status === 412) {
10165
- const { message = "Unexpected error generating upload url" } = await res.json();
10166
- throw new Error(message);
10167
- }
10168
- const { signedUrl, requestId } = await res.json();
10169
- if (!signedUrl) {
10170
- throw new Error("Unexpected error generating upload url");
10171
- }
10172
- const uploadRes = await this.fetchFunction(signedUrl, {
10173
- method: "PUT",
10174
- body: item.file,
10175
- headers: {
10176
- "Content-Type": item.file.type || "application/octet-stream",
10177
- "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
+ }
10178
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
10179
10369
  });
10180
- if (!uploadRes.ok) {
10181
- const xmlRes = await uploadRes.text();
10182
- const matches = s3ErrorRegex.exec(xmlRes);
10183
- console.error(xmlRes);
10184
- if (!matches) {
10185
- 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);
10186
10425
  } else {
10187
- throw new Error(`Upload error: '${matches[2]}'`);
10426
+ return;
10188
10427
  }
10189
10428
  }
10190
- const updateStartTime = Date.now();
10191
- while (true) {
10192
- await new Promise((resolve) => setTimeout(resolve, 1e3));
10193
- const { error: error2, message } = await this.api.getRequestStatus(requestId);
10194
- if (error2 !== void 0) {
10195
- if (error2) {
10196
- throw new Error(message);
10197
- } else {
10198
- break;
10199
- }
10200
- }
10201
- if (Date.now() - updateStartTime > 3e4) {
10202
- throw new Error("Time out waiting for upload to complete");
10203
- }
10429
+ if (Date.now() - startTime > 3e4) {
10430
+ throw new Error(timeoutMessage);
10204
10431
  }
10205
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
+ }
10206
10458
  return this.fetchUploadedEntries(media);
10207
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
+ }
10208
10541
  /**
10209
10542
  * Resolves the just-uploaded items to canonical `Media` entries by hitting
10210
10543
  * the assets-api `list` endpoint, which is the source of truth for the
@@ -10218,15 +10551,7 @@ class TinaMediaStore {
10218
10551
  * throwing — the upload itself already succeeded.
10219
10552
  */
10220
10553
  async fetchUploadedEntries(media) {
10221
- const byDirectory = /* @__PURE__ */ new Map();
10222
- for (const item of media) {
10223
- let dir = item.directory || "";
10224
- while (dir.endsWith("/"))
10225
- dir = dir.slice(0, -1);
10226
- const bucket = byDirectory.get(dir) ?? [];
10227
- bucket.push(item);
10228
- byDirectory.set(dir, bucket);
10229
- }
10554
+ const byDirectory = this.groupMediaByDirectory(media);
10230
10555
  const thumbnailSizes = [
10231
10556
  { w: 75, h: 75 },
10232
10557
  { w: 400, h: 400 },
@@ -10422,37 +10747,33 @@ class TinaMediaStore {
10422
10747
  };
10423
10748
  }
10424
10749
  async delete(media) {
10425
- const path = `${media.directory ? `${media.directory}/${media.filename}` : media.filename}`;
10750
+ const path = this.joinMediaPath(media.directory, media.filename);
10426
10751
  if (!this.isLocal) {
10427
10752
  if (await this.isAuthenticated()) {
10428
- const encodedBranch = this.encodedBranchParam();
10429
- const branchQuery = encodedBranch ? `?branch=${encodedBranch}` : "";
10430
- const res = await this.api.authProvider.fetchWithToken(
10431
- `${this.url}/${path}${branchQuery}`,
10432
- {
10433
- method: "DELETE"
10434
- }
10753
+ const decision = await this.prepareProtectedMediaBranch(
10754
+ "delete",
10755
+ media.directory,
10756
+ media.filename
10435
10757
  );
10436
- 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
+ }
10437
10769
  const { requestId } = await res.json();
10438
- const deleteStartTime = Date.now();
10439
- while (true) {
10440
- await new Promise((resolve) => setTimeout(resolve, 1e3));
10441
- const { error: error2, message } = await this.api.getRequestStatus(requestId);
10442
- if (error2 !== void 0) {
10443
- if (error2) {
10444
- throw new Error(message);
10445
- } else {
10446
- break;
10447
- }
10448
- }
10449
- if (Date.now() - deleteStartTime > 3e4) {
10450
- throw new Error("Time out waiting for delete to complete");
10451
- }
10770
+ if (decision.kind !== "workflow") {
10771
+ await this.waitForRequestStatus(
10772
+ requestId,
10773
+ "Time out waiting for delete to complete"
10774
+ );
10452
10775
  }
10453
- } else {
10454
- throw new Error("Unexpected error deleting media asset");
10455
- }
10776
+ });
10456
10777
  } else {
10457
10778
  throw E_UNAUTHORIZED;
10458
10779
  }
@@ -12865,7 +13186,7 @@ const NavProvider = ({
12865
13186
  const name = "tinacms";
12866
13187
  const type = "module";
12867
13188
  const typings = "dist/index.d.ts";
12868
- const version$1 = "3.8.4";
13189
+ const version$1 = "3.9.0";
12869
13190
  const main = "dist/index.js";
12870
13191
  const module = "./dist/index.js";
12871
13192
  const exports = {
@@ -15454,23 +15775,6 @@ const BranchPreviewButton = (props) => {
15454
15775
  /* @__PURE__ */ React.createElement(BiLinkExternal, { className: "h-5 w-auto" })
15455
15776
  );
15456
15777
  };
15457
- const EDITORIAL_WORKFLOW_STATUS = {
15458
- QUEUED: "queued",
15459
- PROCESSING: "processing",
15460
- SETTING_UP: "setting_up",
15461
- CREATING_BRANCH: "creating_branch",
15462
- INDEXING: "indexing",
15463
- CONTENT_GENERATION: "content_generation",
15464
- CREATING_PR: "creating_pr",
15465
- COMPLETE: "complete",
15466
- ERROR: "error",
15467
- TIMEOUT: "timeout"
15468
- };
15469
- const EDITORIAL_WORKFLOW_ERROR = {
15470
- BRANCH_EXISTS: "BRANCH_EXISTS",
15471
- BRANCH_HIERARCHY_CONFLICT: "BRANCH_HIERARCHY_CONFLICT",
15472
- VALIDATION_FAILED: "VALIDATION_FAILED"
15473
- };
15474
15778
  const CREATE_DOCUMENT_GQL = `#graphql
15475
15779
  mutation($collection: String!, $relativePath: String!, $params: DocumentMutation!) {
15476
15780
  createDocument(
@@ -15828,6 +16132,18 @@ const pathRelativeToCollection = (collectionPath, fullPath) => {
15828
16132
  `Path ${fullPath} not within collection path ${collectionPath}`
15829
16133
  );
15830
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
+ };
15831
16147
  const WORKFLOW_STEPS = [
15832
16148
  { id: 1, name: "Creating branch", description: "Setting up workspace" },
15833
16149
  { id: 2, name: "Updating branch", description: "Syncing content to branch" },
@@ -15838,6 +16154,33 @@ const formatTime = (seconds) => {
15838
16154
  const secs = seconds % 60;
15839
16155
  return `${mins}:${secs.toString().padStart(2, "0")}`;
15840
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
+ };
15841
16184
  function useEditorialWorkflow() {
15842
16185
  const cms = useCMS$1();
15843
16186
  const tinaApi = cms.api.tina;
@@ -15871,20 +16214,30 @@ function useEditorialWorkflow() {
15871
16214
  path,
15872
16215
  values,
15873
16216
  crudType,
15874
- tinaForm
16217
+ tinaForm,
16218
+ signal
15875
16219
  }) => {
15876
16220
  var _a;
15877
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
+ }
15878
16238
  setIsExecuting(true);
15879
16239
  setCurrentStep(1);
15880
- let graphql2 = "";
15881
- if (crudType === "create") {
15882
- graphql2 = CREATE_DOCUMENT_GQL;
15883
- } else if (crudType === "delete") {
15884
- graphql2 = DELETE_DOCUMENT_GQL;
15885
- } else if (crudType !== "view") {
15886
- graphql2 = UPDATE_DOCUMENT_GQL;
15887
- }
16240
+ const graphql2 = getEditorialWorkflowMutation(crudType);
15888
16241
  const collection = tinaApi.schema.getCollectionByFullPath(path);
15889
16242
  let submittedValues = values;
15890
16243
  if ((_a = collection == null ? void 0 : collection.ui) == null ? void 0 : _a.beforeSubmit) {
@@ -15905,7 +16258,7 @@ function useEditorialWorkflow() {
15905
16258
  const result = await tinaApi.executeEditorialWorkflow({
15906
16259
  branchName,
15907
16260
  baseBranch,
15908
- prTitle: `${branchName.replace("tina/", "").replace("-", " ")} (PR from TinaCMS)`,
16261
+ prTitle: getEditorialWorkflowPrTitle(branchName),
15909
16262
  graphQLContentOp: {
15910
16263
  query: graphql2,
15911
16264
  variables: {
@@ -15954,30 +16307,7 @@ function useEditorialWorkflow() {
15954
16307
  return true;
15955
16308
  } catch (e) {
15956
16309
  console.error(e);
15957
- 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 🦙";
15958
- const err = e;
15959
- if (err.errorCode) {
15960
- switch (err.errorCode) {
15961
- case EDITORIAL_WORKFLOW_ERROR.BRANCH_EXISTS:
15962
- errMessage = "A branch with this name already exists";
15963
- break;
15964
- case EDITORIAL_WORKFLOW_ERROR.BRANCH_HIERARCHY_CONFLICT:
15965
- errMessage = err.message || "Branch name conflicts with an existing branch";
15966
- break;
15967
- case EDITORIAL_WORKFLOW_ERROR.VALIDATION_FAILED:
15968
- errMessage = err.message || "Invalid branch name";
15969
- break;
15970
- default:
15971
- errMessage = err.message || errMessage;
15972
- break;
15973
- }
15974
- } else if (err.message) {
15975
- if (err.message.toLowerCase().includes("already exists")) {
15976
- errMessage = "A branch with this name already exists";
15977
- } else if (err.message.toLowerCase().includes("conflict")) {
15978
- errMessage = err.message;
15979
- }
15980
- }
16310
+ const errMessage = getEditorialWorkflowErrorMessage(e);
15981
16311
  setErrorMessage(errMessage);
15982
16312
  setIsExecuting(false);
15983
16313
  setCurrentStep(0);
@@ -16065,6 +16395,18 @@ const WorkflowProgressIndicator = ({
16065
16395
  "Learn more about Editorial Workflow"
16066
16396
  ));
16067
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
+ ))));
16068
16410
  const formatDefaultBranchName = (filePath, crudType) => {
16069
16411
  let result = filePath;
16070
16412
  const contentPrefix = "content/";
@@ -16096,6 +16438,7 @@ const CreateBranchModal = ({
16096
16438
  formatDefaultBranchName(path, crudType)
16097
16439
  );
16098
16440
  const [isBranchGuardChecking, setIsBranchGuardChecking] = React.useState(false);
16441
+ const branchGuardAbortRef = React.useRef(null);
16099
16442
  const {
16100
16443
  isExecuting,
16101
16444
  errorMessage,
@@ -16104,27 +16447,35 @@ const CreateBranchModal = ({
16104
16447
  executeWorkflow,
16105
16448
  reset
16106
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
+ }, []);
16107
16462
  const executeEditorialWorkflow = async () => {
16463
+ abortBranchGuard();
16464
+ const abortController = new AbortController();
16465
+ branchGuardAbortRef.current = abortController;
16108
16466
  setIsBranchGuardChecking(true);
16109
16467
  const baseBranch = decodeURIComponent(tinaApi.branch);
16110
- let baseBranchExists = true;
16111
- try {
16112
- console.debug(
16113
- "[tina:branch-guard] executeEditorialWorkflow: checking base branch:",
16114
- baseBranch
16115
- );
16116
- baseBranchExists = await tinaApi.branchExists(baseBranch);
16117
- } catch (err) {
16118
- console.error(
16119
- "[tina:branch-guard] executeEditorialWorkflow: branchExists threw, failing open:",
16120
- err
16121
- );
16122
- }
16123
- console.debug(
16124
- "[tina:branch-guard] executeEditorialWorkflow: base branch exists?",
16125
- baseBranchExists
16468
+ const targetBranch = `tina/${newBranchName}`;
16469
+ const baseBranchExists = await checkBaseBranchExists(
16470
+ tinaApi,
16471
+ baseBranch,
16472
+ "executeEditorialWorkflow",
16473
+ abortController.signal
16126
16474
  );
16475
+ if (abortController.signal.aborted)
16476
+ return;
16127
16477
  if (!baseBranchExists) {
16478
+ abortBranchGuard();
16128
16479
  console.debug(
16129
16480
  "[tina:branch-guard] executeEditorialWorkflow: base branch deleted — handing off"
16130
16481
  );
@@ -16133,52 +16484,84 @@ const CreateBranchModal = ({
16133
16484
  }
16134
16485
  setIsBranchGuardChecking(false);
16135
16486
  const success = await executeWorkflow({
16136
- branchName: `tina/${newBranchName}`,
16487
+ branchName: targetBranch,
16137
16488
  baseBranch,
16138
16489
  path,
16139
16490
  values,
16140
16491
  crudType,
16141
- tinaForm
16492
+ tinaForm,
16493
+ signal: abortController.signal
16142
16494
  });
16495
+ if (branchGuardAbortRef.current === abortController) {
16496
+ branchGuardAbortRef.current = null;
16497
+ }
16143
16498
  if (success) {
16144
16499
  close2();
16145
16500
  }
16146
16501
  };
16147
- const renderStateContent = () => {
16148
- if (isExecuting) {
16149
- return /* @__PURE__ */ React.createElement(
16150
- WorkflowProgressIndicator,
16151
- {
16152
- currentStep,
16153
- isExecuting,
16154
- elapsedTime
16155
- }
16156
- );
16157
- } else {
16158
- 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(
16159
- "a",
16160
- {
16161
- className: "underline text-tina-orange-dark font-medium",
16162
- href: "https://tina.io/docs/r/editorial-workflow",
16163
- target: "_blank"
16164
- },
16165
- "Editorial Workflow"
16166
- ), "."), /* @__PURE__ */ React.createElement(
16167
- PrefixedTextField,
16168
- {
16169
- name: "new-branch-name",
16170
- label: "Branch Name",
16171
- placeholder: "e.g. {{PAGE-NAME}}-updates",
16172
- value: newBranchName,
16173
- onChange: (e) => {
16174
- reset();
16175
- setNewBranchName(e.target.value);
16176
- }
16177
- }
16178
- ));
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
+ }
16179
16533
  }
16180
- };
16181
- 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(
16182
16565
  Button$2,
16183
16566
  {
16184
16567
  variant: "secondary",
@@ -16192,26 +16575,17 @@ const CreateBranchModal = ({
16192
16575
  variant: "primary",
16193
16576
  align: "start",
16194
16577
  className: "w-full sm:w-auto",
16195
- disabled: newBranchName === "" || isBranchGuardChecking,
16196
- onMainAction: executeEditorialWorkflow,
16578
+ disabled,
16579
+ onMainAction: onCreateBranch,
16197
16580
  items: [
16198
16581
  {
16199
16582
  label: "Save to Protected Branch",
16200
- onClick: () => {
16201
- close2();
16202
- safeSubmit();
16203
- },
16583
+ onClick: onSaveToProtectedBranch,
16204
16584
  icon: /* @__PURE__ */ React.createElement(TriangleAlert, { className: "w-4 h-4" })
16205
16585
  }
16206
16586
  ]
16207
16587
  },
16208
- /* @__PURE__ */ React.createElement(
16209
- GitBranchIcon,
16210
- {
16211
- className: "w-4 h-4 mr-1",
16212
- style: { fill: "none" }
16213
- }
16214
- ),
16588
+ /* @__PURE__ */ React.createElement(GitBranchIcon, { className: "w-4 h-4 mr-1", style: { fill: "none" } }),
16215
16589
  "Save to a new branch"
16216
16590
  ))));
16217
16591
  };
@@ -69994,6 +70368,191 @@ function useLocalStorage(key, initialValue) {
69994
70368
  };
69995
70369
  return [storedValue, setValue];
69996
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
+ };
69997
70556
  function asyncPoll(fn, pollInterval = 5 * 1e3, pollTimeout = 30 * 1e3) {
69998
70557
  const endTime = (/* @__PURE__ */ new Date()).getTime() + pollTimeout;
69999
70558
  let stop = false;
@@ -70553,7 +71112,7 @@ mutation addPendingDocumentMutation(
70553
71112
  }
70554
71113
  });
70555
71114
  if (!res.ok) {
70556
- 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}`;
70557
71116
  if (res.status === 422) {
70558
71117
  errorMessage = `Please make sure you have made changes on ${branch} before creating a pull request.`;
70559
71118
  }
@@ -70562,7 +71121,7 @@ mutation addPendingDocumentMutation(
70562
71121
  const values = await res.json();
70563
71122
  return values;
70564
71123
  } catch (error2) {
70565
- console.error("There was an error creating a new branch.", error2);
71124
+ console.error("There was an error creating a pull request.", error2);
70566
71125
  throw error2;
70567
71126
  }
70568
71127
  }
@@ -70659,7 +71218,8 @@ mutation addPendingDocumentMutation(
70659
71218
  try {
70660
71219
  const url = `${this.contentApiBase}/github/${this.clientId}/list_branches`;
70661
71220
  const res = await this.authProvider.fetchWithToken(url, {
70662
- method: "GET"
71221
+ method: "GET",
71222
+ signal: args == null ? void 0 : args.signal
70663
71223
  });
70664
71224
  const branches = await res.json();
70665
71225
  const parsedBranches = await ListBranchResponse.parseAsync(branches);
@@ -70685,10 +71245,13 @@ mutation addPendingDocumentMutation(
70685
71245
  var _a;
70686
71246
  return this.usingEditorialWorkflow && ((_a = this.protectedBranches) == null ? void 0 : _a.includes(decodeURIComponent(this.branch)));
70687
71247
  }
70688
- async branchExists(branchName) {
71248
+ async branchExists(branchName, args) {
70689
71249
  if (this.isLocalMode)
70690
71250
  return true;
70691
- 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
+ });
70692
71255
  return branches.some((b) => b.name === branchName);
70693
71256
  }
70694
71257
  async createBranch({ baseBranch, branchName }) {
@@ -70732,6 +71295,89 @@ mutation addPendingDocumentMutation(
70732
71295
  throw error2;
70733
71296
  }
70734
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
+ }
70735
71381
  /**
70736
71382
  * Initiate and poll for the results of an editorial workflow operation
70737
71383
  *
@@ -70741,32 +71387,16 @@ mutation addPendingDocumentMutation(
70741
71387
  async executeEditorialWorkflow(options) {
70742
71388
  const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}`;
70743
71389
  try {
70744
- const res = await this.authProvider.fetchWithToken(url, {
70745
- method: "POST",
70746
- body: JSON.stringify({
71390
+ const responseBody = await this.postEditorialWorkflow(
71391
+ url,
71392
+ {
70747
71393
  branchName: options.branchName,
70748
71394
  baseBranch: options.baseBranch,
70749
71395
  prTitle: options.prTitle,
70750
71396
  graphQLContentOp: options.graphQLContentOp
70751
- }),
70752
- headers: {
70753
- "Content-Type": "application/json"
70754
- }
70755
- });
70756
- const responseBody = await res.json();
70757
- if (!res.ok) {
70758
- console.error("There was an error starting editorial workflow.");
70759
- const error2 = new Error(
70760
- (responseBody == null ? void 0 : responseBody.message) || "Failed to start editorial workflow"
70761
- );
70762
- if (responseBody == null ? void 0 : responseBody.errorCode) {
70763
- error2.errorCode = responseBody.errorCode;
70764
- }
70765
- if (responseBody == null ? void 0 : responseBody.conflictingBranch) {
70766
- error2.conflictingBranch = responseBody.conflictingBranch;
70767
- }
70768
- throw error2;
70769
- }
71397
+ },
71398
+ "Failed to start editorial workflow"
71399
+ );
70770
71400
  const requestId = responseBody.requestId;
70771
71401
  if (!requestId) {
70772
71402
  return responseBody;
@@ -70777,60 +71407,10 @@ mutation addPendingDocumentMutation(
70777
71407
  message: "Workflow queued, starting..."
70778
71408
  });
70779
71409
  }
70780
- const maxAttempts = 60;
70781
- const pollInterval = 5e3;
70782
- let attempts = 0;
70783
- while (attempts < maxAttempts) {
70784
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
70785
- attempts++;
70786
- try {
70787
- const statusUrl = `${this.contentApiBase}/editorial-workflow/${this.clientId}/status/${requestId}`;
70788
- const statusResponse = await this.authProvider.fetchWithToken(statusUrl);
70789
- const statusResponseBody = await statusResponse.json();
70790
- if (options.onStatusUpdate) {
70791
- options.onStatusUpdate({
70792
- status: statusResponseBody.status,
70793
- message: statusResponseBody.message || `Status: ${statusResponseBody.status}`
70794
- });
70795
- }
70796
- if (statusResponseBody.status === EDITORIAL_WORKFLOW_STATUS.ERROR || statusResponse.status === 500) {
70797
- if (statusResponseBody.pullRequestUrl) {
70798
- return {
70799
- branchName: statusResponseBody.branchName,
70800
- pullRequestUrl: statusResponseBody.pullRequestUrl,
70801
- warning: statusResponseBody.message
70802
- };
70803
- }
70804
- const error2 = new Error(
70805
- statusResponseBody.message || "Editorial workflow failed"
70806
- );
70807
- error2.errorCode = statusResponseBody.errorCode || "WORKFLOW_FAILED";
70808
- throw error2;
70809
- }
70810
- if (statusResponse.status === 200) {
70811
- return {
70812
- branchName: statusResponseBody.branchName,
70813
- pullRequestUrl: statusResponseBody.pullRequestUrl
70814
- };
70815
- }
70816
- if (statusResponse.status !== 202) {
70817
- const error2 = new Error(
70818
- statusResponseBody.message || `Failed to check workflow status: ${statusResponse.statusText}`
70819
- );
70820
- error2.errorCode = "WORKFLOW_STATUS_FAILED";
70821
- throw error2;
70822
- }
70823
- } catch (error2) {
70824
- if (error2.errorCode) {
70825
- throw error2;
70826
- }
70827
- console.warn(
70828
- `Editorial workflow status poll failed (attempt ${attempts}/${maxAttempts}), retrying...`,
70829
- error2
70830
- );
70831
- }
70832
- }
70833
- throw new Error("Editorial workflow timed out after 5 minutes");
71410
+ return await this.pollEditorialWorkflowStatus(
71411
+ requestId,
71412
+ options.onStatusUpdate
71413
+ );
70834
71414
  } catch (error2) {
70835
71415
  console.error(
70836
71416
  "There was an error with editorial workflow operation.",
@@ -70839,6 +71419,17 @@ mutation addPendingDocumentMutation(
70839
71419
  throw error2;
70840
71420
  }
70841
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
+ }
70842
71433
  }
70843
71434
  const DEFAULT_LOCAL_TINA_GQL_SERVER_URL = "http://localhost:4001/graphql";
70844
71435
  class LocalClient extends Client {
@@ -71392,7 +71983,7 @@ const TinaCloudProvider = (props) => {
71392
71983
  }, []);
71393
71984
  React__default.useEffect(() => {
71394
71985
  const setupEditorialWorkflow = () => {
71395
- client.getProject().then((project) => {
71986
+ client.getProject().then(async (project) => {
71396
71987
  var _a2;
71397
71988
  if ((_a2 = project == null ? void 0 : project.features) == null ? void 0 : _a2.includes("editorial-workflow")) {
71398
71989
  cms.flags.set("branch-switcher", true);
@@ -71422,7 +72013,7 @@ const TinaCloudProvider = (props) => {
71422
72013
  setCurrentBranch(b);
71423
72014
  }
71424
72015
  },
71425
- /* @__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 }))
71426
72017
  ));
71427
72018
  };
71428
72019
  const TinaCloudAuthWall = TinaCloudProvider;
@@ -74457,6 +75048,7 @@ export {
74457
75048
  ColorPicker,
74458
75049
  CreateBranchModal,
74459
75050
  CreateBranchModel,
75051
+ CreateBranchPromptModal,
74460
75052
  CursorPaginator,
74461
75053
  DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
74462
75054
  DEFAULT_MEDIA_UPLOAD_TYPES,
@@ -74535,6 +75127,7 @@ export {
74535
75127
  MediaIcon,
74536
75128
  MediaListError,
74537
75129
  MediaManager$1 as MediaManager,
75130
+ MediaWorkflowOverlay,
74538
75131
  Message,
74539
75132
  Modal,
74540
75133
  ModalActions,