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.
- package/dist/ai/generate.d.ts +4 -4
- package/dist/ai/generate.js +75 -14
- package/dist/ai/types.d.ts +12 -0
- package/dist/api/branch.js +13 -13
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +11 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +2 -1
- package/dist/cli/cli.js +160 -28
- package/dist/cli/generate-ai-content.js +156 -24
- package/dist/content-reader.js +2 -1
- package/dist/context.js +2 -1
- package/dist/editor/BranchManager.js +13 -17
- package/dist/editor/CommentsPanel.js +7 -6
- package/dist/editor/Editor.js +152 -30
- package/dist/editor/EditorAuthGate.js +2 -2
- package/dist/editor/EditorPanes.d.ts +10 -0
- package/dist/editor/EditorPanes.js +32 -8
- package/dist/editor/FormRenderer.d.ts +10 -0
- package/dist/editor/FormRenderer.js +92 -73
- package/dist/editor/PreviewFrame.d.ts +3 -1
- package/dist/editor/PreviewFrame.js +72 -9
- package/dist/editor/admin/SystemHealthPanel.js +27 -24
- package/dist/editor/branch-status.d.ts +16 -0
- package/dist/editor/branch-status.js +28 -0
- package/dist/editor/comments/BranchComments.js +1 -1
- package/dist/editor/comments/EntryComments.d.ts +3 -0
- package/dist/editor/comments/EntryComments.js +3 -3
- package/dist/editor/comments/FieldWrapper.d.ts +3 -0
- package/dist/editor/comments/FieldWrapper.js +2 -2
- package/dist/editor/comments/InlineCommentThread.js +2 -1
- package/dist/editor/comments/ThreadCarousel.js +2 -2
- package/dist/editor/components/ConfirmDeleteModal.js +1 -1
- package/dist/editor/components/EditorCrashScreen.js +1 -1
- package/dist/editor/components/EditorHeader.d.ts +2 -0
- package/dist/editor/components/EditorHeader.js +15 -36
- package/dist/editor/components/EditorSidebar.d.ts +5 -6
- package/dist/editor/components/EditorSidebar.js +2 -2
- package/dist/editor/components/EntryCreateModal.js +1 -1
- package/dist/editor/components/NoEditPermissionNotice.d.ts +3 -2
- package/dist/editor/components/NoEditPermissionNotice.js +3 -2
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/RenameEntryModal.js +1 -1
- package/dist/editor/components/StagedChangesDrawer.js +3 -3
- package/dist/editor/components/UndoToastMessage.d.ts +14 -0
- package/dist/editor/components/UndoToastMessage.js +9 -0
- package/dist/editor/components/UserBadge.js +9 -5
- package/dist/editor/copy.d.ts +14 -0
- package/dist/editor/copy.js +14 -0
- package/dist/editor/fields/BlockField.d.ts +5 -0
- package/dist/editor/fields/BlockField.js +38 -32
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +5 -2
- package/dist/editor/fields/DateTimeField.d.ts +2 -0
- package/dist/editor/fields/DateTimeField.js +5 -2
- package/dist/editor/fields/FieldCrashFallback.d.ts +3 -3
- package/dist/editor/fields/FieldCrashFallback.js +4 -4
- package/dist/editor/fields/FieldLabel.d.ts +24 -0
- package/dist/editor/fields/FieldLabel.js +9 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +17 -8
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/MarkdownField.d.ts +6 -0
- package/dist/editor/fields/MarkdownField.js +41 -11
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +4 -2
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +4 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -4
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +9 -3
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +8 -2
- package/dist/editor/fields/StringListField.d.ts +2 -0
- package/dist/editor/fields/StringListField.js +5 -2
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +5 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +6 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/group-manager/GroupCard.js +1 -1
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +1 -1
- package/dist/editor/group-manager/MemberList.js +1 -1
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +1 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +1 -1
- package/dist/editor/group-manager/index.js +26 -2
- package/dist/editor/hooks/useBranchActions.js +2 -2
- package/dist/editor/hooks/useBranchManager.js +36 -22
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.js +27 -5
- package/dist/editor/hooks/useEditorLayout.d.ts +45 -3
- package/dist/editor/hooks/useEditorLayout.js +97 -7
- package/dist/editor/hooks/useUserMetadata.js +12 -1
- package/dist/editor/media/AssetCard.js +1 -1
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibraryBody.js +3 -3
- package/dist/editor/permission-manager/PermissionEditor.js +3 -3
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.js +2 -2
- package/dist/editor/schema-editor/CollectionEditor.js +1 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +1 -1
- package/dist/editor/theme.d.ts +1 -1
- package/dist/editor/theme.js +11 -11
- package/dist/editor/undo-removal.d.ts +44 -0
- package/dist/editor/undo-removal.js +129 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/git-manager.d.ts +6 -6
- package/dist/git-manager.js +16 -15
- package/dist/github-service.d.ts +25 -1
- package/dist/github-service.js +65 -6
- package/dist/http/handler.js +2 -1
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/types.d.ts +9 -1
- package/dist/utils/error.js +4 -3
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +68 -17
- package/dist/worker/cms-worker.js +116 -33
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +8 -7
- package/dist/worker/git-sync.js +184 -74
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +589 -0
- package/dist/worker/github-gateway-wire.js +484 -0
- package/dist/worker/github-gateway.d.ts +152 -58
- package/dist/worker/github-gateway.js +485 -129
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +271 -71
- package/dist/worker/github-mirror.js +755 -238
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +20 -7
- package/dist/worker/shared-repo-git.js +51 -22
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +2 -0
- package/dist/worker/task-runner.js +49 -20
- package/dist/worker/worker-context.d.ts +6 -0
- 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+/]
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
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
|
|
6343
|
-
|
|
6437
|
+
if (pending.contentId === null)
|
|
6438
|
+
continue;
|
|
6439
|
+
exported.delete(pending.contentId);
|
|
6440
|
+
failedIds.add(pending.contentId);
|
|
6344
6441
|
}
|
|
6345
|
-
round = allPending.filter((pending) =>
|
|
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
|
-
|
|
6433
|
-
|
|
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
|
|
7682
|
-
*
|
|
7683
|
-
*
|
|
7684
|
-
*
|
|
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
|
|
9398
|
+
return asRecord2(JSON.parse(raw));
|
|
9267
9399
|
}
|
|
9268
9400
|
if (format === "yaml") {
|
|
9269
|
-
return
|
|
9401
|
+
return asRecord2(yamlParse2(raw));
|
|
9270
9402
|
}
|
|
9271
9403
|
const parsed = matter3(raw);
|
|
9272
9404
|
const data = { ...parsed.data ?? {} };
|
package/dist/content-reader.js
CHANGED
|
@@ -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 ??
|
|
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:
|
|
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:
|
|
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 ===
|
|
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",
|
|
114
|
-
const
|
|
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,
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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))) })) })] }) }));
|