canopycms 0.0.68-int.113 → 0.0.68-int.114

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 (170) hide show
  1. package/dist/ai/generate.d.ts +4 -4
  2. package/dist/ai/generate.js +75 -14
  3. package/dist/ai/types.d.ts +12 -0
  4. package/dist/api/branch.js +13 -13
  5. package/dist/auth/cache.d.ts +1 -1
  6. package/dist/auth/cache.js +1 -1
  7. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  8. package/dist/auth/file-based-auth-cache.js +11 -0
  9. package/dist/authorization/branch.d.ts +1 -1
  10. package/dist/authorization/branch.js +2 -1
  11. package/dist/cli/cli.js +160 -28
  12. package/dist/cli/generate-ai-content.js +156 -24
  13. package/dist/content-reader.js +2 -1
  14. package/dist/context.js +2 -1
  15. package/dist/editor/BranchManager.js +13 -17
  16. package/dist/editor/CommentsPanel.js +7 -6
  17. package/dist/editor/Editor.js +152 -30
  18. package/dist/editor/EditorAuthGate.js +2 -2
  19. package/dist/editor/EditorPanes.d.ts +10 -0
  20. package/dist/editor/EditorPanes.js +32 -8
  21. package/dist/editor/FormRenderer.d.ts +10 -0
  22. package/dist/editor/FormRenderer.js +92 -73
  23. package/dist/editor/PreviewFrame.d.ts +3 -1
  24. package/dist/editor/PreviewFrame.js +72 -9
  25. package/dist/editor/admin/SystemHealthPanel.js +27 -24
  26. package/dist/editor/branch-status.d.ts +16 -0
  27. package/dist/editor/branch-status.js +28 -0
  28. package/dist/editor/comments/BranchComments.js +1 -1
  29. package/dist/editor/comments/EntryComments.d.ts +3 -0
  30. package/dist/editor/comments/EntryComments.js +3 -3
  31. package/dist/editor/comments/FieldWrapper.d.ts +3 -0
  32. package/dist/editor/comments/FieldWrapper.js +2 -2
  33. package/dist/editor/comments/InlineCommentThread.js +2 -1
  34. package/dist/editor/comments/ThreadCarousel.js +2 -2
  35. package/dist/editor/components/ConfirmDeleteModal.js +1 -1
  36. package/dist/editor/components/EditorCrashScreen.js +1 -1
  37. package/dist/editor/components/EditorHeader.d.ts +2 -0
  38. package/dist/editor/components/EditorHeader.js +15 -36
  39. package/dist/editor/components/EditorSidebar.d.ts +5 -6
  40. package/dist/editor/components/EditorSidebar.js +2 -2
  41. package/dist/editor/components/EntryCreateModal.js +1 -1
  42. package/dist/editor/components/NoEditPermissionNotice.d.ts +3 -2
  43. package/dist/editor/components/NoEditPermissionNotice.js +3 -2
  44. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  45. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  46. package/dist/editor/components/RenameEntryModal.js +1 -1
  47. package/dist/editor/components/StagedChangesDrawer.js +3 -3
  48. package/dist/editor/components/UndoToastMessage.d.ts +14 -0
  49. package/dist/editor/components/UndoToastMessage.js +9 -0
  50. package/dist/editor/components/UserBadge.js +9 -5
  51. package/dist/editor/copy.d.ts +14 -0
  52. package/dist/editor/copy.js +14 -0
  53. package/dist/editor/fields/BlockField.d.ts +5 -0
  54. package/dist/editor/fields/BlockField.js +38 -32
  55. package/dist/editor/fields/CodeField.d.ts +2 -0
  56. package/dist/editor/fields/CodeField.js +5 -2
  57. package/dist/editor/fields/DateTimeField.d.ts +2 -0
  58. package/dist/editor/fields/DateTimeField.js +5 -2
  59. package/dist/editor/fields/FieldCrashFallback.d.ts +3 -3
  60. package/dist/editor/fields/FieldCrashFallback.js +4 -4
  61. package/dist/editor/fields/FieldLabel.d.ts +24 -0
  62. package/dist/editor/fields/FieldLabel.js +9 -0
  63. package/dist/editor/fields/ImageField.d.ts +4 -0
  64. package/dist/editor/fields/ImageField.js +17 -8
  65. package/dist/editor/fields/InlineGroupField.js +4 -3
  66. package/dist/editor/fields/MarkdownField.d.ts +6 -0
  67. package/dist/editor/fields/MarkdownField.js +41 -11
  68. package/dist/editor/fields/MdxImageDialog.js +1 -1
  69. package/dist/editor/fields/NumberField.d.ts +2 -0
  70. package/dist/editor/fields/NumberField.js +4 -2
  71. package/dist/editor/fields/NumberListField.d.ts +2 -0
  72. package/dist/editor/fields/NumberListField.js +4 -2
  73. package/dist/editor/fields/ObjectField.d.ts +1 -0
  74. package/dist/editor/fields/ObjectField.js +5 -4
  75. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  76. package/dist/editor/fields/ReferenceField.js +9 -3
  77. package/dist/editor/fields/SelectField.d.ts +2 -0
  78. package/dist/editor/fields/SelectField.js +8 -2
  79. package/dist/editor/fields/StringListField.d.ts +2 -0
  80. package/dist/editor/fields/StringListField.js +5 -2
  81. package/dist/editor/fields/TextField.d.ts +2 -0
  82. package/dist/editor/fields/TextField.js +5 -2
  83. package/dist/editor/fields/ToggleField.d.ts +2 -0
  84. package/dist/editor/fields/ToggleField.js +6 -3
  85. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  86. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  87. package/dist/editor/fields/list-item-keys.js +50 -0
  88. package/dist/editor/group-manager/GroupCard.js +1 -1
  89. package/dist/editor/group-manager/GroupForm.js +1 -1
  90. package/dist/editor/group-manager/InternalGroupsTab.js +1 -1
  91. package/dist/editor/group-manager/MemberList.js +1 -1
  92. package/dist/editor/group-manager/hooks/useGroupState.d.ts +1 -0
  93. package/dist/editor/group-manager/hooks/useGroupState.js +1 -1
  94. package/dist/editor/group-manager/index.js +26 -2
  95. package/dist/editor/hooks/useBranchActions.js +2 -2
  96. package/dist/editor/hooks/useBranchManager.js +36 -22
  97. package/dist/editor/hooks/useDraftManager.d.ts +8 -0
  98. package/dist/editor/hooks/useDraftManager.js +27 -5
  99. package/dist/editor/hooks/useEditorLayout.d.ts +45 -3
  100. package/dist/editor/hooks/useEditorLayout.js +97 -7
  101. package/dist/editor/hooks/useUserMetadata.js +12 -1
  102. package/dist/editor/media/AssetCard.js +1 -1
  103. package/dist/editor/media/CropStep.js +6 -1
  104. package/dist/editor/media/MediaLibraryBody.js +3 -3
  105. package/dist/editor/permission-manager/PermissionEditor.js +3 -3
  106. package/dist/editor/permission-manager/index.js +1 -1
  107. package/dist/editor/preview-bridge.js +2 -2
  108. package/dist/editor/schema-editor/CollectionEditor.js +1 -1
  109. package/dist/editor/schema-editor/EntryTypeEditor.js +1 -1
  110. package/dist/editor/theme.d.ts +1 -1
  111. package/dist/editor/theme.js +11 -11
  112. package/dist/editor/undo-removal.d.ts +44 -0
  113. package/dist/editor/undo-removal.js +129 -0
  114. package/dist/editor/user-display.d.ts +4 -0
  115. package/dist/editor/user-display.js +5 -0
  116. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  117. package/dist/editor/utils/confirm-modal.js +12 -0
  118. package/dist/entry-link-resolver.d.ts +11 -1
  119. package/dist/entry-link-resolver.js +65 -5
  120. package/dist/git-manager.d.ts +6 -6
  121. package/dist/git-manager.js +16 -15
  122. package/dist/github-service.d.ts +25 -1
  123. package/dist/github-service.js +65 -6
  124. package/dist/http/handler.js +2 -1
  125. package/dist/static/seo.d.ts +5 -5
  126. package/dist/static/seo.js +5 -5
  127. package/dist/system-users.d.ts +12 -0
  128. package/dist/system-users.js +12 -0
  129. package/dist/types.d.ts +9 -1
  130. package/dist/utils/error.js +4 -3
  131. package/dist/utils/escape-text.d.ts +7 -0
  132. package/dist/utils/escape-text.js +27 -0
  133. package/dist/version.d.ts +1 -1
  134. package/dist/version.js +1 -1
  135. package/dist/worker/branch-policy.d.ts +124 -0
  136. package/dist/worker/branch-policy.js +243 -0
  137. package/dist/worker/bundle-header.d.ts +117 -0
  138. package/dist/worker/bundle-header.js +253 -0
  139. package/dist/worker/cms-worker.d.ts +68 -17
  140. package/dist/worker/cms-worker.js +116 -33
  141. package/dist/worker/gateway-egress.d.ts +43 -0
  142. package/dist/worker/gateway-egress.js +209 -0
  143. package/dist/worker/gateway-instance.d.ts +42 -0
  144. package/dist/worker/gateway-instance.js +129 -0
  145. package/dist/worker/git-process.d.ts +81 -0
  146. package/dist/worker/git-process.js +234 -0
  147. package/dist/worker/git-sync.d.ts +8 -7
  148. package/dist/worker/git-sync.js +184 -74
  149. package/dist/worker/github-gateway-http.d.ts +62 -0
  150. package/dist/worker/github-gateway-http.js +977 -0
  151. package/dist/worker/github-gateway-wire.d.ts +589 -0
  152. package/dist/worker/github-gateway-wire.js +484 -0
  153. package/dist/worker/github-gateway.d.ts +152 -58
  154. package/dist/worker/github-gateway.js +485 -129
  155. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  156. package/dist/worker/github-mirror-upkeep.js +266 -0
  157. package/dist/worker/github-mirror.d.ts +271 -71
  158. package/dist/worker/github-mirror.js +755 -238
  159. package/dist/worker/github-transfer.d.ts +56 -0
  160. package/dist/worker/github-transfer.js +444 -0
  161. package/dist/worker/self-checks.d.ts +107 -0
  162. package/dist/worker/self-checks.js +358 -0
  163. package/dist/worker/shared-repo-git.d.ts +20 -7
  164. package/dist/worker/shared-repo-git.js +51 -22
  165. package/dist/worker/stale-mirror.d.ts +82 -0
  166. package/dist/worker/stale-mirror.js +130 -0
  167. package/dist/worker/task-runner.d.ts +2 -0
  168. package/dist/worker/task-runner.js +49 -20
  169. package/dist/worker/worker-context.d.ts +6 -0
  170. package/package.json +1 -1
@@ -22,7 +22,7 @@ function redactCredentials(message) {
22
22
  let result = message;
23
23
  result = result.replace(/(:\/\/)[^/\s@]+@/g, "$1***@");
24
24
  result = result.replace(/(\bauthorization\s*:\s*(?:basic|bearer|token)\s+)[A-Za-z0-9+/=._~-]+/gi, "$1***");
25
- result = result.replace(/eC1hY2Nlc3MtdG9rZW46[A-Za-z0-9+/]*={0,2}/g, "***");
25
+ result = result.replace(/eC1hY2Nlc3MtdG9rZW46(?:[A-Za-z0-9+/]|%2[BbFf])*(?:=|%3[Dd]){0,2}/g, "***");
26
26
  result = result.replace(/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{8,}/g, "***");
27
27
  result = result.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/g, "$1***");
28
28
  result = result.replace(/-----BEGIN [A-Z]{0,9} ?PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z]{0,9} ?PRIVATE KEY-----|$)/g, "<private-key>");
@@ -2684,6 +2684,45 @@ var DEFAULT_SEO_FIELD_NAMES = {
2684
2684
  noindex: "noindex",
2685
2685
  twitterCard: "twitterCard"
2686
2686
  };
2687
+ var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2688
+ var asText = (value) => {
2689
+ if (typeof value !== "string")
2690
+ return void 0;
2691
+ const trimmed = value.trim();
2692
+ return trimmed === "" ? void 0 : trimmed;
2693
+ };
2694
+ var asFlag = (value) => typeof value === "boolean" ? value : void 0;
2695
+ var asImageUrl = (value) => {
2696
+ const text = asText(value);
2697
+ if (text)
2698
+ return text;
2699
+ return asText(asRecord(value)?.src);
2700
+ };
2701
+ var asOgType = (value) => {
2702
+ const text = asText(value);
2703
+ return text === "website" || text === "article" || text === "profile" ? text : void 0;
2704
+ };
2705
+ var asTwitterCard = (value) => {
2706
+ const text = asText(value);
2707
+ return text === "summary" || text === "summary_large_image" ? text : void 0;
2708
+ };
2709
+ function extractSeoFields(entryData, opts = {}) {
2710
+ const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
2711
+ const entry = asRecord(entryData);
2712
+ const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
2713
+ return {
2714
+ title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
2715
+ description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
2716
+ ogImage: asImageUrl(source?.[names.ogImage]),
2717
+ ogType: asOgType(source?.[names.ogType]),
2718
+ canonical: asText(source?.[names.canonical]),
2719
+ noindex: asFlag(source?.[names.noindex]),
2720
+ twitterCard: asTwitterCard(source?.[names.twitterCard])
2721
+ };
2722
+ }
2723
+ function isNoindexEntry(entryData, opts = {}) {
2724
+ return extractSeoFields(entryData, opts).noindex === true;
2725
+ }
2687
2726
 
2688
2727
  // dist/entry-schema.js
2689
2728
  function buildResolvedReference(data, meta, body) {
@@ -4240,7 +4279,7 @@ var getFormatExtension = (format) => {
4240
4279
  return ".json";
4241
4280
  };
4242
4281
  var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
4243
- var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4282
+ var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4244
4283
 
4245
4284
  // dist/paths/index.js
4246
4285
  init_normalize();
@@ -4812,7 +4851,7 @@ var ContentStore = class {
4812
4851
  fields = entryTypeConfig?.schema || [];
4813
4852
  }
4814
4853
  if (format === "json") {
4815
- const data = asRecord(JSON.parse(raw));
4854
+ const data = asRecord2(JSON.parse(raw));
4816
4855
  doc = {
4817
4856
  collection: schemaItem.logicalPath,
4818
4857
  collectionName: schemaItem.name,
@@ -4822,7 +4861,7 @@ var ContentStore = class {
4822
4861
  absolutePath
4823
4862
  };
4824
4863
  } else if (format === "yaml") {
4825
- const data = asRecord(yamlParse(raw));
4864
+ const data = asRecord2(yamlParse(raw));
4826
4865
  doc = {
4827
4866
  collection: schemaItem.logicalPath,
4828
4867
  collectionName: schemaItem.name,
@@ -6196,17 +6235,21 @@ var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`,
6196
6235
  function resolveEntryUrl(location, contentRoot) {
6197
6236
  return computeEntryUrl(location.collection ?? "", location.slug ?? "", contentRoot);
6198
6237
  }
6199
- function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
6238
+ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver, options = {}) {
6239
+ const { isLinkable } = options;
6200
6240
  const parts = splitByCodeRegions(text);
6201
6241
  return parts.map((part) => {
6202
6242
  if (part.isCode)
6203
6243
  return part.text;
6204
- return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
6205
- const location = idIndex.findById(id);
6206
- if (!location || location.type !== "entry" || !location.collection || !location.slug) {
6244
+ const partText = isLinkable ? unwrapInlineLinks(part.text, (id) => !isLinkable(id) || !findEntry(idIndex, id)) : part.text;
6245
+ return partText.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
6246
+ const location = findEntry(idIndex, id);
6247
+ if (!location) {
6207
6248
  log6.warn("resolve", `Entry link target not found: entry:${id}`);
6208
- return anchor ?? "#";
6249
+ return isLinkable ? "#" : anchor ?? "#";
6209
6250
  }
6251
+ if (isLinkable && !isLinkable(id))
6252
+ return "#";
6210
6253
  let url;
6211
6254
  if (customResolver) {
6212
6255
  url = customResolver({
@@ -6221,6 +6264,46 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
6221
6264
  });
6222
6265
  }).join("");
6223
6266
  }
6267
+ function findEntry(idIndex, id) {
6268
+ const location = idIndex.findById(id);
6269
+ if (!location || location.type !== "entry" || !location.collection || !location.slug) {
6270
+ return void 0;
6271
+ }
6272
+ return location;
6273
+ }
6274
+ var INLINE_LINK_TARGET = new RegExp(`\\]\\(entry:(${BASE58_CHAR}{12})(?:#[^\\s)>"'\\]]*)?(?:[ \\t]+"[^"\\n]*")?\\)`, "g");
6275
+ function unwrapInlineLinks(text, shouldUnwrap) {
6276
+ let out = "";
6277
+ let cursor = 0;
6278
+ let scanFloor = 0;
6279
+ for (const match of text.matchAll(INLINE_LINK_TARGET)) {
6280
+ const close = match.index;
6281
+ if (close < cursor || !shouldUnwrap(match[1]))
6282
+ continue;
6283
+ const open = findOpeningBracket(text, close, Math.max(cursor, scanFloor));
6284
+ if (open === -1) {
6285
+ scanFloor = close;
6286
+ continue;
6287
+ }
6288
+ const isImage = open > cursor && text[open - 1] === "!" && text[open - 2] !== "\\";
6289
+ const start = isImage ? open - 1 : open;
6290
+ out += text.slice(cursor, start) + text.slice(open + 1, close);
6291
+ cursor = close + match[0].length;
6292
+ }
6293
+ return out + text.slice(cursor);
6294
+ }
6295
+ function findOpeningBracket(text, close, floor) {
6296
+ let depth = 1;
6297
+ for (let i = close - 1; i >= floor; i--) {
6298
+ if (i > floor && text[i - 1] === "\\")
6299
+ continue;
6300
+ if (text[i] === "]")
6301
+ depth++;
6302
+ else if (text[i] === "[" && --depth === 0)
6303
+ return i;
6304
+ }
6305
+ return -1;
6306
+ }
6224
6307
  function splitByCodeRegions(text) {
6225
6308
  const parts = [];
6226
6309
  let current = "";
@@ -6295,6 +6378,7 @@ function splitByCodeRegions(text) {
6295
6378
  // dist/ai/generate.js
6296
6379
  async function generateAIContent(options) {
6297
6380
  const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
6381
+ assertValidNoindexConfig(config);
6298
6382
  const files = /* @__PURE__ */ new Map();
6299
6383
  const idIndex = await store.idIndex();
6300
6384
  const read = {
@@ -6331,6 +6415,16 @@ async function generateAIContent(options) {
6331
6415
  let round = allPending;
6332
6416
  for (; ; ) {
6333
6417
  for (const pending of round) {
6418
+ if (pending.body !== void 0) {
6419
+ const linked = /* @__PURE__ */ new Set();
6420
+ pending.entry.body = resolveEntryLinksInText(pending.body, idIndex, contentRoot, entryLinkUrl, {
6421
+ isLinkable: (id) => {
6422
+ linked.add(id);
6423
+ return exported.has(id);
6424
+ }
6425
+ });
6426
+ pending.linkedIds = [...linked];
6427
+ }
6334
6428
  maskUnexportedTargets(pending.entry, exported);
6335
6429
  pending.entry.appendedSections = void 0;
6336
6430
  await runEntryTransform(pending.entry, pending.absolutePath, pending.contentId, config);
@@ -6338,11 +6432,19 @@ async function generateAIContent(options) {
6338
6432
  const failed = renderAll(round, config, references);
6339
6433
  if (failed.length === 0)
6340
6434
  break;
6435
+ const failedIds = /* @__PURE__ */ new Set();
6341
6436
  for (const pending of failed) {
6342
- if (pending.contentId !== null)
6343
- exported.delete(pending.contentId);
6437
+ if (pending.contentId === null)
6438
+ continue;
6439
+ exported.delete(pending.contentId);
6440
+ failedIds.add(pending.contentId);
6344
6441
  }
6345
- round = allPending.filter((pending) => isRendered(pending) && maskUnexportedTargets(pending.entry, exported));
6442
+ round = allPending.filter((pending) => {
6443
+ if (!isRendered(pending))
6444
+ return false;
6445
+ const masked = maskUnexportedTargets(pending.entry, exported);
6446
+ return masked || pending.linkedIds.some((id) => failedIds.has(id));
6447
+ });
6346
6448
  }
6347
6449
  const manifestCollections = collectionNodes.map((node) => emitCollection(node, files));
6348
6450
  const manifestRootEntries = emitEntries(rootEntries, files);
@@ -6411,6 +6513,7 @@ async function collectCollection(read, collection, flatSchema) {
6411
6513
  }
6412
6514
  async function collectEntries(read, collection, cleanPath) {
6413
6515
  const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
6516
+ const skipNoindex = config?.exclude?.noindex !== false;
6414
6517
  const pending = [];
6415
6518
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
6416
6519
  const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
@@ -6427,15 +6530,19 @@ async function collectEntries(read, collection, cleanPath) {
6427
6530
  const doc = await store.read(listEntry.collection, listEntry.slug, {
6428
6531
  resolveReferences: false
6429
6532
  });
6533
+ if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
6534
+ continue;
6430
6535
  doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
6431
6536
  const aiEntry = docToAIEntry(doc, listEntry.slug, entryTypeName, entryTypeConfig, cleanPath);
6432
- if (aiEntry.body) {
6433
- aiEntry.body = resolveEntryLinksInText(aiEntry.body, idIndex, contentRoot, entryLinkUrl);
6434
- }
6537
+ const body = aiEntry.body;
6538
+ if (body)
6539
+ aiEntry.body = resolveEntryLinksInText(body, idIndex, contentRoot, entryLinkUrl);
6435
6540
  if (config?.exclude?.where?.(aiEntry))
6436
6541
  continue;
6437
6542
  pending.push({
6438
6543
  entry: aiEntry,
6544
+ body,
6545
+ linkedIds: [],
6439
6546
  contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
6440
6547
  absolutePath: doc.absolutePath,
6441
6548
  collectionPath: collection.logicalPath,
@@ -6476,6 +6583,31 @@ function emitCollection(node, files) {
6476
6583
  subcollections: manifestSubcollections.length > 0 ? manifestSubcollections : void 0
6477
6584
  };
6478
6585
  }
6586
+ function assertValidNoindexConfig(config) {
6587
+ const noindex = config?.exclude?.noindex;
6588
+ if (noindex !== void 0 && typeof noindex !== "boolean") {
6589
+ throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
6590
+ }
6591
+ const seo = config?.seo;
6592
+ if (seo === void 0)
6593
+ return;
6594
+ if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
6595
+ throw new Error("AI content config: seo must be an object of { group?, fields? }");
6596
+ }
6597
+ const { group, fields } = seo;
6598
+ if (group !== void 0 && typeof group !== "string") {
6599
+ throw new Error("AI content config: seo.group must be a string");
6600
+ }
6601
+ if (fields === void 0)
6602
+ return;
6603
+ if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
6604
+ throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
6605
+ }
6606
+ const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
6607
+ if (unknown.length > 0) {
6608
+ throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(", ")}`);
6609
+ }
6610
+ }
6479
6611
  function stripContentRoot(logicalPath, contentRoot) {
6480
6612
  if (logicalPath.startsWith(contentRoot + "/")) {
6481
6613
  return logicalPath.slice(contentRoot.length + 1);
@@ -7322,7 +7454,7 @@ async function ensureRemoteGitConfig(gitDir, git = simpleGit2().env(gitChildEnv(
7322
7454
  var REMOTE_GIT_MAX_LOOSE_OBJECTS = 50;
7323
7455
  var REMOTE_GIT_MAX_PACKS = 6;
7324
7456
  async function countObjects(git, gitDir) {
7325
- const output = await git.raw(["--git-dir", gitDir, "count-objects", "-v"]);
7457
+ const output = await git.raw(["--git-dir", gitDir, "count-objects", "-v", "--end-of-options"]);
7326
7458
  const fields = new Map(output.split("\n").map((line) => {
7327
7459
  const [name, value = ""] = line.split(": ");
7328
7460
  return [name, value.trim()];
@@ -7342,8 +7474,8 @@ async function repackBareRemoteIfNeeded(gitDir, git = simpleGit2().env(gitChildE
7342
7474
  return { repacked: false, before };
7343
7475
  }
7344
7476
  const startedAt = Date.now();
7345
- await git.raw(["--git-dir", gitDir, "repack", "-a", "-d", "--cruft", "-q"]);
7346
- await git.raw(["--git-dir", gitDir, "pack-refs", "--all"]);
7477
+ await git.raw(["--git-dir", gitDir, "repack", "-a", "-d", "--cruft", "-q", "--end-of-options"]);
7478
+ await git.raw(["--git-dir", gitDir, "pack-refs", "--all", "--end-of-options"]);
7347
7479
  const after = await countObjects(git, gitDir);
7348
7480
  return { repacked: true, before, after, ms: Date.now() - startedAt };
7349
7481
  }
@@ -7678,10 +7810,10 @@ var GitManager = class _GitManager {
7678
7810
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
7679
7811
  * no-op (not an error) when the ref doesn't exist.
7680
7812
  *
7681
- * The explicit path for removing a deleted branch's local head, which the
7682
- * sync loop deliberately is not (reconcileTrackedBranches never deletes a
7683
- * head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
7684
- * deleteBranchHandler: a head left in `remote.git` forever makes the
7813
+ * The path that removes a deleted branch's local head at once; the sync
7814
+ * loop's reconcileTrackedBranches removes one only after GitHub no longer
7815
+ * has the branch. Called by api/branch.ts's deleteBranchHandler: a head
7816
+ * left in `remote.git` makes the
7685
7817
  * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
7686
7818
  * the reused branch's first publish non-fast-forward against the stale head
7687
7819
  * (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
@@ -9263,10 +9395,10 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
9263
9395
  try {
9264
9396
  const raw = await fs20.readFile(filePath, "utf8");
9265
9397
  if (format === "json") {
9266
- return asRecord(JSON.parse(raw));
9398
+ return asRecord2(JSON.parse(raw));
9267
9399
  }
9268
9400
  if (format === "yaml") {
9269
- return asRecord(yamlParse2(raw));
9401
+ return asRecord2(yamlParse2(raw));
9270
9402
  }
9271
9403
  const parsed = matter3(raw);
9272
9404
  const data = { ...parsed.data ?? {} };
@@ -8,6 +8,7 @@ import { isDeployedStatic, isBuildMode, readsFromCheckout } from './build-mode.j
8
8
  import { isNotFoundError } from './utils/error.js';
9
9
  import { resolveEntryLinksInData } from './entry-link-resolver.js';
10
10
  import { baseBranchOf } from './utils/base-branch.js';
11
+ import { CONTENT_READER_USER_ID } from './system-users.js';
11
12
  /**
12
13
  * Server-side helper to read content directly from a branch workspace. A branch with no workspace
13
14
  * reads as NOT_FOUND unless `allowCreateBranch` or a `getBranchContext` resolver says otherwise.
@@ -18,7 +19,7 @@ export const createContentReader = (options) => {
18
19
  const basePathOverride = options.basePathOverride;
19
20
  const defaultBranch = options.defaultBranch ?? services.config.defaultActiveBranch ?? baseBranchOf(services.config);
20
21
  const allowCreateBranch = options.allowCreateBranch ?? false;
21
- const createdBy = options.createdBy ?? 'canopycms-content-reader';
22
+ const createdBy = options.createdBy ?? CONTENT_READER_USER_ID;
22
23
  const resolveBranchContext = async (branchName) => {
23
24
  // Static deployments and builds read from the checkout: loadOrCreateBranchContext
24
25
  // returns a synthetic cwd context without git ops, regardless of allowCreateBranch
package/dist/context.js CHANGED
@@ -8,6 +8,7 @@ import { buildContentTree as buildContentTreeImpl, } from './content-tree.js';
8
8
  import { listEntries as listEntriesImpl, } from './content-listing.js';
9
9
  import { createDebugLogger } from './utils/debug.js';
10
10
  import { baseBranchOf } from './utils/base-branch.js';
11
+ import { CONTEXT_USER_ID } from './system-users.js';
11
12
  const log = createDebugLogger({ prefix: 'Context' });
12
13
  /** True when a ContentStoreError indicates a path/entry wasn't found (expected during candidate probing). */
13
14
  function isLookupFailure(err) {
@@ -114,7 +115,7 @@ export function createCanopyContext(options) {
114
115
  config: services.config,
115
116
  branchName: activeBranch,
116
117
  mode: services.config.mode,
117
- createdBy: 'canopycms-context',
118
+ createdBy: CONTEXT_USER_ID,
118
119
  remoteUrl: services.config.defaultRemoteUrl,
119
120
  });
120
121
  }
@@ -4,10 +4,13 @@ import { useRef, useState } from 'react';
4
4
  import { Badge, Button, Group, Paper, ScrollArea, Stack, Text, TextInput, Textarea, Collapse, Tooltip, } from '@mantine/core';
5
5
  import { BranchComments } from './comments/BranchComments.js';
6
6
  import { UserBadge } from './components/UserBadge.js';
7
+ import { userIdLabel } from './user-display.js';
7
8
  // Import directly from helpers to avoid server-only code in authorization barrel
8
9
  import { isAdmin, isReviewer } from '../authorization/helpers.js';
9
10
  import { clientOperatingStrategy } from '../operating-mode/client.js';
10
11
  import { formatRelativeTime } from './relative-time.js';
12
+ import { PROTECTED_BRANCH_PRESENTATION, branchStatusPresentation } from './branch-status.js';
13
+ import { SYSTEM_USER_ID } from '../system-users.js';
11
14
  /**
12
15
  * Compute what actions the current user can perform on a branch.
13
16
  * Uses the same hybrid permission model as the backend:
@@ -30,9 +33,9 @@ export const getBranchPermissions = (branch, user) => {
30
33
  const userIsReviewer = isReviewer(user.groups);
31
34
  const userIsCreator = branch.createdBy === user.userId;
32
35
  // The system-branch grant is disabled on the protected base branch -- its
33
- // auto-provision marker (createdBy: 'canopycms-system') would otherwise let
36
+ // auto-provision marker (createdBy: SYSTEM_USER_ID) would otherwise let
34
37
  // anyone with general access submit/withdraw/delete it.
35
- const isSystemBranch = branch.createdBy === 'canopycms-system' && !branch.isProtected;
38
+ const isSystemBranch = branch.createdBy === SYSTEM_USER_ID && !branch.isProtected;
36
39
  const userInACL = (branch.access?.users?.includes(user.userId) ||
37
40
  user.groups?.some((g) => branch.access?.groups?.includes(g))) ??
38
41
  false;
@@ -72,10 +75,6 @@ export const getBranchPermissions = (branch, user) => {
72
75
  branch.pullRequestState !== 'closed';
73
76
  return { canSubmit, canWithdraw, canDelete, canRequestChanges };
74
77
  };
75
- const statusColorMap = {
76
- editing: { color: 'brand', variant: 'light' },
77
- submitted: { color: 'green', variant: 'light' },
78
- };
79
78
  export const BranchManager = ({ branches, mode, user, onSelect, onBeforeCreate, onCreate, onDelete, onSubmit, onWithdraw, onRequestChanges, onClose: _, comments = [], currentUserId, canResolve = false, onAddComment, onResolveThread, highlightThreadId, onGetUserMetadata, }) => {
80
79
  const supportsBranching = clientOperatingStrategy(mode).supportsBranching();
81
80
  const [showCreateForm, setShowCreateForm] = useState(false);
@@ -110,21 +109,18 @@ export const BranchManager = ({ branches, mode, user, onSelect, onBeforeCreate,
110
109
  setCreating(false);
111
110
  }
112
111
  };
113
- return (_jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, "data-testid": "branch-manager", children: [currentUserId && onAddComment && onResolveThread && (_jsx(Stack, { pt: "sm", children: _jsx(BranchComments, { comments: comments, currentUserId: currentUserId, canResolve: canResolve, onAddComment: onAddComment, onResolveThread: onResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: onGetUserMetadata }) })), !!supportsBranching && (_jsxs(Stack, { gap: "sm", pt: "sm", children: [_jsx(Button, { variant: "light", size: "sm", fullWidth: true, onClick: () => setShowCreateForm(!showCreateForm), disabled: creating, "data-testid": "create-branch-button", children: showCreateForm ? 'Cancel' : 'Create New Branch' }), _jsx(Collapse, { in: showCreateForm, children: _jsx(Paper, { withBorder: true, p: "md", radius: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsx(TextInput, { label: "Branch Name", placeholder: "feature/my-branch", value: newBranchName, onChange: (e) => setNewBranchName(e.target.value), required: true, disabled: creating, "data-testid": "branch-name-input" }), _jsx(TextInput, { label: "Title (optional)", placeholder: "Brief description", value: newBranchTitle, onChange: (e) => setNewBranchTitle(e.target.value), disabled: creating, "data-testid": "branch-title-input" }), _jsx(Textarea, { label: "Description (optional)", placeholder: "Detailed description of the changes", value: newBranchDescription, onChange: (e) => setNewBranchDescription(e.target.value), minRows: 2, disabled: creating, "data-testid": "branch-description-textarea" }), _jsx(Button, { onClick: handleCreate, disabled: !newBranchName.trim(), loading: creating, fullWidth: true, "data-testid": "create-branch-submit", children: "Create Branch" })] }) }) })] })), _jsx(ScrollArea, { style: { flex: 1 }, pb: "md", children: branches.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", py: "md", children: !supportsBranching ? 'Branch management is not available.' : 'No branches available.' })) : (_jsx(Stack, { gap: "sm", children: branches.map((b) => {
114
- const statusColor = statusColorMap[b.status] ?? {
115
- color: 'neutral',
116
- variant: 'light',
117
- };
112
+ return (_jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, "data-testid": "branch-manager", children: [currentUserId && onAddComment && onResolveThread && (_jsx(Stack, { pt: "sm", children: _jsx(BranchComments, { comments: comments, currentUserId: currentUserId, canResolve: canResolve, onAddComment: onAddComment, onResolveThread: onResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: onGetUserMetadata }) })), !!supportsBranching && (_jsxs(Stack, { gap: "sm", pt: "sm", children: [_jsx(Button, { variant: "light", size: "sm", fullWidth: true, onClick: () => setShowCreateForm(!showCreateForm), disabled: creating, "data-testid": "create-branch-button", children: showCreateForm ? 'Cancel' : 'Create New Branch' }), _jsx(Collapse, { in: showCreateForm, children: _jsx(Paper, { withBorder: true, p: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsx(TextInput, { label: "Branch Name", placeholder: "feature/my-branch", value: newBranchName, onChange: (e) => setNewBranchName(e.target.value), required: true, disabled: creating, "data-testid": "branch-name-input" }), _jsx(TextInput, { label: "Title (optional)", placeholder: "Brief description", value: newBranchTitle, onChange: (e) => setNewBranchTitle(e.target.value), disabled: creating, "data-testid": "branch-title-input" }), _jsx(Textarea, { label: "Description (optional)", placeholder: "Detailed description of the changes", value: newBranchDescription, onChange: (e) => setNewBranchDescription(e.target.value), minRows: 2, disabled: creating, "data-testid": "branch-description-textarea" }), _jsx(Button, { size: "sm", onClick: handleCreate, disabled: !newBranchName.trim(), loading: creating, fullWidth: true, "data-testid": "create-branch-submit", children: "Create Branch" })] }) }) })] })), _jsx(ScrollArea, { style: { flex: 1 }, pb: "md", children: branches.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", py: "md", children: !supportsBranching ? 'Branch management is not available.' : 'No branches available.' })) : (_jsx(Stack, { gap: "sm", children: branches.map((b) => {
113
+ const statusBadge = branchStatusPresentation(b.status);
118
114
  const perms = getBranchPermissions(b, user);
119
- return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", shadow: "xs", "data-testid": `branch-list-item-${b.name}`, children: _jsxs(Group, { justify: "space-between", align: "flex-start", children: [_jsxs(Stack, { gap: 4, children: [_jsxs(Group, { gap: "xs", children: [_jsx(Text, { fw: 600, children: b.name }), _jsx(Badge, { color: statusColor.color, variant: statusColor.variant, "data-testid": `branch-status-badge-${b.name}`, children: b.status }), b.status === 'archived' && b.mergedAt && (_jsx(Badge, { color: "teal", variant: "light", "data-testid": `branch-merged-badge-${b.name}`, children: "Merged" })), b.isProtected && (_jsx(Badge, { color: "neutral", variant: "outline", "data-testid": `branch-protected-badge-${b.name}`, children: "Protected" })), b.pullRequestNumber && (_jsxs(Badge, { color: b.pullRequestState === 'closed'
115
+ return (_jsx(Paper, { withBorder: true, p: "md", shadow: "xs", "data-testid": `branch-list-item-${b.name}`, children: _jsxs(Group, { justify: "space-between", align: "flex-start", children: [_jsxs(Stack, { gap: 4, children: [_jsxs(Group, { gap: "xs", children: [_jsx(Text, { fw: 600, children: b.name }), statusBadge && (_jsx(Badge, { color: statusBadge.color, variant: statusBadge.variant, "data-testid": `branch-status-badge-${b.name}`, "data-status": b.status, children: statusBadge.label })), b.status === 'archived' && b.mergedAt && (_jsx(Badge, { color: "teal", variant: "light", "data-testid": `branch-merged-badge-${b.name}`, children: "Merged" })), b.isProtected && (_jsx(Badge, { color: PROTECTED_BRANCH_PRESENTATION.color, variant: PROTECTED_BRANCH_PRESENTATION.variant, "data-testid": `branch-protected-badge-${b.name}`, children: PROTECTED_BRANCH_PRESENTATION.label })), b.pullRequestNumber && (_jsxs(Badge, { color: b.pullRequestState === 'closed'
120
116
  ? 'red'
121
117
  : b.pullRequestState === 'merged'
122
118
  ? 'violet'
123
119
  : 'blue', variant: "light", children: ["PR #", b.pullRequestNumber, b.pullRequestState === 'closed' ? ' (closed)' : ''] })), b.commentCount !== undefined && b.commentCount > 0 && (_jsxs(Badge, { color: "grape", variant: "light", children: [b.commentCount, " ", b.commentCount === 1 ? 'comment' : 'comments'] })), b.syncStatus === 'sync-failed' && (_jsx(Tooltip, { label: "GitHub sync failed \u2014 an admin can retry it from System health.", disabled: Boolean(b.syncFailureReason), multiline: true, maw: 320, children: _jsx(Badge, { color: "red", variant: "light", "data-testid": `sync-failed-badge-${b.name}`, children: "Sync failed" }) })), b.syncStatus === 'pending-sync' && (_jsx(Tooltip, { label: "Changes are on their way to GitHub.", children: _jsx(Badge, { color: "gray", variant: "light", "data-testid": `pending-sync-badge-${b.name}`, children: "Syncing\u2026" }) })), b.conflictStatus === 'conflicts-detected' && (_jsx(Tooltip, { label: "This branch's version was kept for conflicting entries during a rebase \u2014 review the flagged entries.", children: _jsxs(Badge, { color: "orange", variant: "light", "data-testid": `conflicts-badge-${b.name}`, children: ["Conflicts", b.conflictFiles?.length ? ` (${b.conflictFiles.length})` : ''] }) }))] }), b.syncStatus === 'sync-failed' && b.syncFailureReason && (_jsxs(Text, { size: "xs", c: "red", maw: 480, "data-testid": `sync-failed-reason-${b.name}`, children: [b.syncFailureReason, b.status === 'submitted' &&
124
- ' Withdraw this branch to unlock it for editing.'] })), _jsxs(Group, { gap: "xs", align: "center", children: [b.updatedAt && (_jsx(Tooltip, { label: new Date(b.updatedAt).toLocaleString(), children: _jsxs(Text, { size: "xs", c: "dimmed", children: ["Updated ", formatRelativeTime(b.updatedAt)] }) })), b.createdBy && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", c: "dimmed", children: "\u2022" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Owner:" }), onGetUserMetadata ? (_jsx(UserBadge, { userId: b.createdBy, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", showEmailTooltip: true, showBadge: true, badgeVariant: "light", color: "gray" })) : (_jsx(Text, { size: "xs", c: "dimmed", children: b.createdBy }))] })), b.pullRequestUrl && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", c: "dimmed", children: "\u2022" }), _jsx(Text, { size: "xs", c: "blue", component: "a", href: b.pullRequestUrl, target: "_blank", rel: "noopener noreferrer", style: {
120
+ ' Withdraw this branch to unlock it for editing.'] })), _jsxs(Group, { gap: "xs", align: "center", children: [b.updatedAt && (_jsx(Tooltip, { label: new Date(b.updatedAt).toLocaleString(), children: _jsxs(Text, { size: "xs", c: "dimmed", children: ["Updated ", formatRelativeTime(b.updatedAt)] }) })), b.createdBy && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", c: "dimmed", children: "\u2022" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Owner:" }), onGetUserMetadata ? (_jsx(UserBadge, { userId: b.createdBy, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", showEmailTooltip: true, showBadge: true, badgeVariant: "light", color: "gray" })) : (_jsx(Text, { size: "xs", c: "dimmed", children: userIdLabel(b.createdBy) }))] })), b.pullRequestUrl && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", c: "dimmed", children: "\u2022" }), _jsx(Text, { size: "xs", c: "blue", component: "a", href: b.pullRequestUrl, target: "_blank", rel: "noopener noreferrer", style: {
125
121
  textDecoration: 'underline',
126
122
  cursor: 'pointer',
127
- }, children: "View PR" })] }))] }), b.access && (_jsxs(Group, { gap: 6, children: [b.access.users?.map((u) => onGetUserMetadata ? (_jsx(UserBadge, { userId: u, getUserMetadata: onGetUserMetadata, variant: "avatar-only", size: "xs", showEmailTooltip: true }, u)) : (_jsx(Badge, { variant: "outline", color: "neutral", children: u }, u))), b.access.groups?.map((g) => (_jsx(Badge, { variant: "outline", color: "neutral", children: g }, g)))] }))] }), _jsxs(Group, { gap: 8, children: [_jsx(Button, { size: "xs", variant: "light", onClick: () => onSelect?.(b.name), "data-testid": `switch-to-branch-button-${b.name}`, children: "Open" }), b.status === 'submitted' || b.status === 'approved' ? (_jsx(Tooltip, { label: "Only the branch creator can withdraw", disabled: perms.canWithdraw, children: _jsx(Button, { size: "xs", variant: "light", color: "orange", onClick: () => onWithdraw?.(b.name), disabled: !perms.canWithdraw, "data-testid": `withdraw-branch-button-${b.name}`, children: "Withdraw" }) })) : (_jsx(Tooltip, { label: b.isProtected
123
+ }, children: "View PR" })] }))] }), b.access && (_jsxs(Group, { gap: 6, children: [b.access.users?.map((u) => onGetUserMetadata ? (_jsx(UserBadge, { userId: u, getUserMetadata: onGetUserMetadata, variant: "avatar-only", size: "xs", showEmailTooltip: true }, u)) : (_jsx(Badge, { variant: "outline", color: "neutral", children: u }, u))), b.access.groups?.map((g) => (_jsx(Badge, { variant: "outline", color: "neutral", children: g }, g)))] }))] }), _jsxs(Group, { gap: 8, children: [_jsx(Button, { variant: "light", onClick: () => onSelect?.(b.name), "data-testid": `switch-to-branch-button-${b.name}`, children: "Open" }), b.status === 'submitted' || b.status === 'approved' ? (_jsx(Tooltip, { label: "Only the branch creator can withdraw", disabled: perms.canWithdraw, children: _jsx(Button, { variant: "light", color: "orange", onClick: () => onWithdraw?.(b.name), disabled: !perms.canWithdraw, "data-testid": `withdraw-branch-button-${b.name}`, children: "Withdraw" }) })) : (_jsx(Tooltip, { label: b.isProtected
128
124
  ? 'The base branch cannot be submitted'
129
125
  : // A status arm: canSubmit folds in the server's status rule as
130
126
  // well as the base-branch one, so an archived (or any
@@ -132,9 +128,9 @@ export const BranchManager = ({ branches, mode, user, onSelect, onBeforeCreate,
132
128
  // generic "Only the branch creator can submit".
133
129
  b.status !== 'editing'
134
130
  ? `A branch with status "${b.status}" cannot be submitted`
135
- : 'Only the branch creator can submit', disabled: perms.canSubmit, children: _jsx(Button, { size: "xs", variant: "light", color: "green", onClick: () => onSubmit?.(b.name), disabled: !perms.canSubmit, "data-testid": `submit-branch-button-${b.name}`, children: "Submit" }) })), _jsx(Tooltip, { label: b.pullRequestState === 'closed'
131
+ : 'Only the branch creator can submit', disabled: perms.canSubmit, children: _jsx(Button, { variant: "light", color: "green", onClick: () => onSubmit?.(b.name), disabled: !perms.canSubmit, "data-testid": `submit-branch-button-${b.name}`, children: "Submit" }) })), _jsx(Tooltip, { label: b.pullRequestState === 'closed'
136
132
  ? 'PR was closed on GitHub without merging'
137
- : 'Only Reviewers or Admins can request changes', disabled: perms.canRequestChanges, children: _jsx(Button, { size: "xs", variant: "outline", color: "neutral", onClick: () => onRequestChanges?.(b.name), disabled: !perms.canRequestChanges, "data-testid": `request-changes-branch-button-${b.name}`, children: "Request changes" }) }), _jsx(Tooltip, { label: b.isProtected
133
+ : 'Only Reviewers or Admins can request changes', disabled: perms.canRequestChanges, children: _jsx(Button, { variant: "outline", color: "neutral", onClick: () => onRequestChanges?.(b.name), disabled: !perms.canRequestChanges, "data-testid": `request-changes-branch-button-${b.name}`, children: "Request changes" }) }), _jsx(Tooltip, { label: b.isProtected
138
134
  ? 'The base branch cannot be deleted'
139
135
  : // A status arm, mirroring the Submit tooltip above: without it,
140
136
  // a branch blocked by its own status reads the generic "Only
@@ -143,6 +139,6 @@ export const BranchManager = ({ branches, mode, user, onSelect, onBeforeCreate,
143
139
  // mean an open PR is blocking deletion.
144
140
  b.status === 'submitted' || b.status === 'approved'
145
141
  ? `Cannot delete branch with open PR (status "${b.status}")`
146
- : 'Only Admin or branch creator can delete', disabled: perms.canDelete, children: _jsx(Button, { size: "xs", variant: "outline", color: "red", onClick: () => onDelete?.(b.name), disabled: !perms.canDelete, "data-testid": `delete-branch-button-${b.name}`, children: "Delete" }) })] })] }) }, b.name));
142
+ : 'Only Admin or branch creator can delete', disabled: perms.canDelete, children: _jsx(Button, { variant: "outline", color: "red", onClick: () => onDelete?.(b.name), disabled: !perms.canDelete, "data-testid": `delete-branch-button-${b.name}`, children: "Delete" }) })] })] }) }, b.name));
147
143
  }) })) })] }));
148
144
  };
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { Badge, Button, Drawer, Group, Paper, ScrollArea, Stack, Text, Textarea, Title, Switch, } from '@mantine/core';
5
5
  import { UserBadge } from './components/UserBadge.js';
6
+ import { userIdLabel } from './user-display.js';
6
7
  import { formatRelativeTime } from './relative-time.js';
7
8
  export const CommentsPanel = ({ branchName, comments, canResolve, onAddComment, onResolveThread, onClose, onJumpToField, onJumpToEntry, onJumpToBranch, onGetUserMetadata, }) => {
8
9
  const [newCommentText, setNewCommentText] = useState('');
@@ -40,26 +41,26 @@ export const CommentsPanel = ({ branchName, comments, canResolve, onAddComment,
40
41
  setIsSubmitting(false);
41
42
  }
42
43
  };
43
- return (_jsx(Drawer, { opened: true, onClose: onClose, position: "right", size: 420, title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Comments" }), _jsx(Text, { size: "xs", c: "dimmed", children: branchName })] }), children: _jsxs(Stack, { gap: "md", h: "100%", style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Paper, { withBorder: true, p: "md", children: _jsxs(Stack, { gap: "sm", children: [replyTo && (_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", c: "dimmed", children: "Replying to thread" }), _jsx(Button, { size: "xs", variant: "subtle", onClick: () => setReplyTo(null), children: "Cancel" })] })), !replyTo && (_jsxs(Text, { size: "xs", c: "dimmed", children: ["Posts a branch-level comment on \"", branchName, "\". Use a field's \"New comment\" link for field-specific threads."] })), _jsx(Textarea, { placeholder: replyTo ? 'Write a reply...' : 'Write a comment...', value: newCommentText, onChange: (e) => setNewCommentText(e.target.value), minRows: 3, disabled: isSubmitting, "data-testid": "comment-textarea" }), _jsx(Button, { onClick: handleAddComment, size: "sm", loading: isSubmitting, disabled: !newCommentText.trim(), "data-testid": "comment-submit", children: replyTo ? 'Reply' : 'Add Comment' })] }) }), _jsxs(Group, { justify: "space-between", children: [_jsxs(Text, { size: "sm", fw: 500, children: [filteredThreads.length, " ", filteredThreads.length === 1 ? 'thread' : 'threads'] }), _jsx(Switch, { label: "Show resolved", checked: showResolved, onChange: (e) => setShowResolved(e.currentTarget.checked), size: "sm" })] }), _jsx(ScrollArea, { style: { flex: 1 }, children: filteredThreads.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", ta: "center", py: "xl", children: showResolved ? 'No comments yet' : 'No unresolved comments' })) : (_jsx(Stack, { gap: "md", children: filteredThreads.map((thread) => (_jsx(Paper, { withBorder: true, p: "md", bg: thread.resolved ? 'gray.0' : undefined, "data-testid": "comment-thread", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { justify: "space-between", align: "flex-start", children: [_jsxs(Group, { gap: "xs", children: [_jsx(Badge, { size: "xs", variant: "outline", color: thread.type === 'field'
44
+ return (_jsx(Drawer, { opened: true, onClose: onClose, position: "right", size: 420, title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Comments" }), _jsx(Text, { size: "xs", c: "dimmed", children: branchName })] }), children: _jsxs(Stack, { gap: "md", h: "100%", style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Paper, { withBorder: true, p: "md", children: _jsxs(Stack, { gap: "sm", children: [replyTo && (_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", c: "dimmed", children: "Replying to thread" }), _jsx(Button, { variant: "subtle", onClick: () => setReplyTo(null), children: "Cancel" })] })), !replyTo && (_jsxs(Text, { size: "xs", c: "dimmed", children: ["Posts a branch-level comment on \"", branchName, "\". Use a field's \"New comment\" link for field-specific threads."] })), _jsx(Textarea, { placeholder: replyTo ? 'Write a reply...' : 'Write a comment...', value: newCommentText, onChange: (e) => setNewCommentText(e.target.value), minRows: 3, disabled: isSubmitting, "data-testid": "comment-textarea" }), _jsx(Button, { onClick: handleAddComment, size: "sm", loading: isSubmitting, disabled: !newCommentText.trim(), "data-testid": "comment-submit", children: replyTo ? 'Reply' : 'Add Comment' })] }) }), _jsxs(Group, { justify: "space-between", children: [_jsxs(Text, { size: "sm", fw: 500, children: [filteredThreads.length, " ", filteredThreads.length === 1 ? 'thread' : 'threads'] }), _jsx(Switch, { label: "Show resolved", checked: showResolved, onChange: (e) => setShowResolved(e.currentTarget.checked), size: "sm" })] }), _jsx(ScrollArea, { style: { flex: 1 }, children: filteredThreads.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", ta: "center", py: "xl", children: showResolved ? 'No comments yet' : 'No unresolved comments' })) : (_jsx(Stack, { gap: "md", children: filteredThreads.map((thread) => (_jsx(Paper, { withBorder: true, p: "md", bg: thread.resolved ? 'gray.0' : undefined, "data-testid": "comment-thread", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { justify: "space-between", align: "flex-start", children: [_jsxs(Group, { gap: "xs", children: [_jsx(Badge, { size: "xs", variant: "outline", color: thread.type === 'field'
44
45
  ? 'blue'
45
46
  : thread.type === 'entry'
46
47
  ? 'grape'
47
- : 'gray', children: thread.type }), thread.entryPath && (_jsx(Badge, { size: "xs", variant: "light", children: thread.entryPath })), thread.canopyPath && (_jsx(Badge, { size: "xs", variant: "dot", children: thread.canopyPath })), thread.resolved && (_jsx(Badge, { size: "xs", color: "green", variant: "light", children: "Resolved" }))] }), _jsx(Group, { gap: "xs", children: canResolve && !thread.resolved && (_jsx(Button, { size: "xs", variant: "subtle", color: "green", onClick: () => handleResolve(thread.id), loading: isSubmitting, children: "Resolve" })) })] }), _jsxs(Group, { gap: "xs", children: [thread.type === 'field' &&
48
+ : 'gray', children: thread.type }), thread.entryPath && (_jsx(Badge, { size: "xs", variant: "light", children: thread.entryPath })), thread.canopyPath && (_jsx(Badge, { size: "xs", variant: "dot", children: thread.canopyPath })), thread.resolved && (_jsx(Badge, { size: "xs", color: "green", variant: "light", children: "Resolved" }))] }), _jsx(Group, { gap: "xs", children: canResolve && !thread.resolved && (_jsx(Button, { variant: "subtle", color: "green", onClick: () => handleResolve(thread.id), loading: isSubmitting, children: "Resolve" })) })] }), _jsxs(Group, { gap: "xs", children: [thread.type === 'field' &&
48
49
  thread.entryPath &&
49
50
  thread.canopyPath &&
50
- onJumpToField && (_jsx(Button, { size: "xs", variant: "light", onClick: () => {
51
+ onJumpToField && (_jsx(Button, { variant: "light", onClick: () => {
51
52
  onJumpToField(thread.entryPath, thread.canopyPath, thread.id);
52
53
  onClose();
53
- }, children: "Jump to field" })), thread.type === 'entry' && thread.entryPath && onJumpToEntry && (_jsx(Button, { size: "xs", variant: "light", onClick: () => {
54
+ }, children: "Jump to field" })), thread.type === 'entry' && thread.entryPath && onJumpToEntry && (_jsx(Button, { variant: "light", onClick: () => {
54
55
  onJumpToEntry(thread.entryPath, thread.id);
55
56
  onClose();
56
- }, children: "Jump to entry" })), thread.type === 'branch' && onJumpToBranch && (_jsx(Button, { size: "xs", variant: "light", onClick: () => {
57
+ }, children: "Jump to entry" })), thread.type === 'branch' && onJumpToBranch && (_jsx(Button, { variant: "light", onClick: () => {
57
58
  onJumpToBranch(thread.id);
58
59
  onClose();
59
60
  }, children: "Open branch discussion" }))] }), thread.comments.map((comment, idx) => (_jsxs("div", { children: [idx > 0 && (_jsx("div", { style: {
60
61
  borderTop: '1px solid var(--mantine-color-gray-3)',
61
62
  margin: '8px 0',
62
- } })), _jsxs(Stack, { gap: 4, children: [_jsxs(Group, { gap: "xs", children: [onGetUserMetadata ? (_jsx(UserBadge, { userId: comment.userId, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", showEmailTooltip: true, showBadge: true, badgeVariant: "light", color: "gray" })) : (_jsx(Text, { size: "xs", fw: 500, children: comment.userId })), _jsx(Text, { size: "xs", c: "dimmed", title: new Date(comment.timestamp).toLocaleString(), children: formatRelativeTime(comment.timestamp) })] }), _jsx(Text, { size: "sm", style: { whiteSpace: 'pre-wrap' }, children: comment.text })] })] }, comment.id))), !thread.resolved && (_jsx(Button, { size: "xs", variant: "subtle", onClick: () => {
63
+ } })), _jsxs(Stack, { gap: 4, children: [_jsxs(Group, { gap: "xs", children: [onGetUserMetadata ? (_jsx(UserBadge, { userId: comment.userId, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", showEmailTooltip: true, showBadge: true, badgeVariant: "light", color: "gray" })) : (_jsx(Text, { size: "xs", fw: 500, children: userIdLabel(comment.userId) })), _jsx(Text, { size: "xs", c: "dimmed", title: new Date(comment.timestamp).toLocaleString(), children: formatRelativeTime(comment.timestamp) })] }), _jsx(Text, { size: "sm", style: { whiteSpace: 'pre-wrap' }, children: comment.text })] })] }, comment.id))), !thread.resolved && (_jsx(Button, { variant: "subtle", onClick: () => {
63
64
  setReplyTo(thread.id);
64
65
  setNewCommentText('');
65
66
  }, mt: "xs", children: "Reply" }))] }) }, thread.id))) })) })] }) }));