canopycms 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +73 -47
  2. package/dist/ai/json-to-markdown.js +2 -0
  3. package/dist/api/admin.d.ts +11 -0
  4. package/dist/api/admin.js +2 -0
  5. package/dist/api/archived-branches.d.ts +70 -0
  6. package/dist/api/archived-branches.js +146 -0
  7. package/dist/api/branch-entry-changes.js +2 -4
  8. package/dist/api/branch-merge.js +16 -1
  9. package/dist/api/branch-status.d.ts +11 -0
  10. package/dist/api/branch-status.js +2 -0
  11. package/dist/api/branch-withdraw.js +1 -1
  12. package/dist/api/branch.js +46 -108
  13. package/dist/api/client.d.ts +25 -21
  14. package/dist/api/client.js +46 -38
  15. package/dist/branch-archive.d.ts +19 -0
  16. package/dist/branch-archive.js +27 -0
  17. package/dist/branch-delete.d.ts +45 -0
  18. package/dist/branch-delete.js +97 -0
  19. package/dist/branch-metadata-file.d.ts +3 -0
  20. package/dist/branch-metadata-file.js +1 -0
  21. package/dist/branch-metadata.d.ts +6 -1
  22. package/dist/branch-metadata.js +15 -2
  23. package/dist/cli/cli.js +85 -16
  24. package/dist/cli/generate-ai-content.js +83 -15
  25. package/dist/cli/init.js +1 -1
  26. package/dist/config/types.d.ts +4 -4
  27. package/dist/config/types.js +1 -1
  28. package/dist/config/validation.d.ts +9 -3
  29. package/dist/config/validation.js +30 -5
  30. package/dist/content-listing.js +2 -3
  31. package/dist/content-store.js +5 -5
  32. package/dist/editor/BranchManager.d.ts +20 -0
  33. package/dist/editor/BranchManager.js +92 -26
  34. package/dist/editor/Editor.js +5 -1
  35. package/dist/editor/FormRenderer.js +5 -0
  36. package/dist/editor/admin/SystemHealthPanel.js +61 -6
  37. package/dist/editor/admin/useSystemHealth.d.ts +3 -0
  38. package/dist/editor/admin/useSystemHealth.js +59 -0
  39. package/dist/editor/branch-status.js +9 -0
  40. package/dist/editor/components/EntryChangesDrawer.js +3 -2
  41. package/dist/editor/copy.d.ts +3 -0
  42. package/dist/editor/copy.js +3 -0
  43. package/dist/editor/entry-diff.js +7 -1
  44. package/dist/editor/fields/DateField.d.ts +19 -0
  45. package/dist/editor/fields/DateField.js +53 -0
  46. package/dist/editor/fields/DateTimeField.d.ts +10 -0
  47. package/dist/editor/fields/DateTimeField.js +5 -15
  48. package/dist/editor/fields/list-item-title.js +2 -1
  49. package/dist/editor/hooks/useBranchManager.d.ts +5 -0
  50. package/dist/editor/hooks/useBranchManager.js +55 -4
  51. package/dist/editor/hooks/useBranchesData.d.ts +2 -0
  52. package/dist/editor/hooks/useBranchesData.js +3 -1
  53. package/dist/editor/theme.js +12 -4
  54. package/dist/entry-schema-registry.js +2 -1
  55. package/dist/static/index.js +5 -2
  56. package/dist/types.d.ts +33 -0
  57. package/dist/utils/calendar-day.d.ts +23 -0
  58. package/dist/utils/calendar-day.js +102 -0
  59. package/dist/utils/content-serialize.d.ts +4 -2
  60. package/dist/utils/content-serialize.js +8 -6
  61. package/dist/validation/entry-validator.js +7 -1
  62. package/dist/validation/field-traversal.js +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/dist/worker/archived-sweep.d.ts +23 -0
  66. package/dist/worker/archived-sweep.js +137 -0
  67. package/dist/worker/cms-worker.d.ts +6 -0
  68. package/dist/worker/cms-worker.js +7 -0
  69. package/dist/worker/git-sync.d.ts +4 -4
  70. package/dist/worker/git-sync.js +3 -0
  71. package/dist/worker/github-commits.d.ts +2 -2
  72. package/dist/worker/github-commits.js +5 -2
  73. package/dist/worker/github-gateway-wire.d.ts +2 -2
  74. package/dist/worker/rebase.js +6 -2
  75. package/dist/worker/task-runner.js +7 -4
  76. package/dist/worker/worker-context.d.ts +2 -0
  77. package/package.json +1 -1
@@ -68,7 +68,7 @@ var init_atomic_write = __esm({
68
68
 
69
69
  // dist/validation/field-traversal.js
70
70
  function isPlainRecord(value) {
71
- return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
72
72
  }
73
73
  function resolveBlockItem(blockField, item) {
74
74
  const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
@@ -573,7 +573,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
573
573
  var init_types2 = __esm({
574
574
  "dist/config/types.js"() {
575
575
  "use strict";
576
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
576
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
577
577
  markdownFieldTypes = ["markdown", "mdx"];
578
578
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
579
579
  fieldTypes = [
@@ -1640,6 +1640,70 @@ function isBlockStructuralKey(key) {
1640
1640
  // dist/utils/content-serialize.js
1641
1641
  init_item_keys();
1642
1642
 
1643
+ // dist/utils/calendar-day.js
1644
+ init_item_keys();
1645
+ init_field_traversal();
1646
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
1647
+ var MS_PER_DAY = 864e5;
1648
+ var isRealDay = (year, month, day) => {
1649
+ const lastOfMonth = /* @__PURE__ */ new Date(0);
1650
+ lastOfMonth.setUTCFullYear(year, month, 0);
1651
+ return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
1652
+ };
1653
+ var dayParts = (value) => {
1654
+ const match = typeof value === "string" ? DAY.exec(value) : null;
1655
+ if (!match)
1656
+ return void 0;
1657
+ const [year, month, day] = match.slice(1).map(Number);
1658
+ return isRealDay(year, month, day) ? [year, month, day] : void 0;
1659
+ };
1660
+ var isCalendarDay = (value) => dayParts(value) !== void 0;
1661
+ var dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
1662
+ var mapItems = (items, mapItem) => {
1663
+ const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
1664
+ return next.every((item, i) => item === items[i]) ? items : next;
1665
+ };
1666
+ var blockItemWithDays = (field, item) => {
1667
+ const resolved = resolveBlockItem(field, item);
1668
+ if (!resolved)
1669
+ return item;
1670
+ const data = withCalendarDays(resolved.fields, resolved.data);
1671
+ if (data === resolved.data)
1672
+ return item;
1673
+ return resolved.data === item ? data : { ...item, value: data };
1674
+ };
1675
+ var valueWithDays = (field, value) => {
1676
+ if (field.type === "date") {
1677
+ return value instanceof Date ? dayOfBareDate(value) ?? value : value;
1678
+ }
1679
+ if (field.type === "object") {
1680
+ const { fields, list } = field;
1681
+ if (list) {
1682
+ return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
1683
+ }
1684
+ return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
1685
+ }
1686
+ if (field.type === "block" && Array.isArray(value)) {
1687
+ return mapItems(value, (item) => blockItemWithDays(field, item));
1688
+ }
1689
+ return value;
1690
+ };
1691
+ function withCalendarDays(fields, data) {
1692
+ let out = data;
1693
+ for (const field of fields) {
1694
+ if (field.type === "group") {
1695
+ out = withCalendarDays(field.fields, out);
1696
+ continue;
1697
+ }
1698
+ const value = out[field.name];
1699
+ const next = valueWithDays(field, value);
1700
+ if (next !== value)
1701
+ out = { ...out, [field.name]: next };
1702
+ }
1703
+ return out;
1704
+ }
1705
+ var entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
1706
+
1643
1707
  // dist/utils/debug.js
1644
1708
  var envFlag = (name) => typeof process !== "undefined" && process.env[name] === "true";
1645
1709
  var LOG_LEVELS = {
@@ -3235,9 +3299,9 @@ function asJson(value) {
3235
3299
  const key = identityKey(value);
3236
3300
  return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
3237
3301
  }
3238
- function frontmatterAsRead(raw) {
3302
+ function frontmatterAsRead(raw, fields) {
3239
3303
  try {
3240
- return asJson(matter(raw, {}).data);
3304
+ return asJson(withCalendarDays(fields, matter(raw, {}).data));
3241
3305
  } catch {
3242
3306
  return void 0;
3243
3307
  }
@@ -3305,7 +3369,7 @@ ${asKey ? `${text}: v` : `v: ${text}`}
3305
3369
  return true;
3306
3370
  }
3307
3371
  }
3308
- function serializeFrontmatter(editorBody, data, existingRaw, format) {
3372
+ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
3309
3373
  const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
3310
3374
  if (existingRaw === void 0)
3311
3375
  return matter.stringify(file, data);
@@ -3313,7 +3377,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3313
3377
  if (existingFrontmatter === void 0)
3314
3378
  return matter.stringify(file, data);
3315
3379
  const onDisk = parseDocument2(existingFrontmatter);
3316
- const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
3380
+ const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
3317
3381
  const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
3318
3382
  if (reconciled === void 0)
3319
3383
  return matter.stringify(file, data);
@@ -3325,7 +3389,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3325
3389
  }
3326
3390
  }
3327
3391
  });
3328
- if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
3392
+ if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
3329
3393
  return written;
3330
3394
  log2.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
3331
3395
  return matter.stringify(file, data);
@@ -3639,9 +3703,6 @@ function linkOnlyReferenceData(data, format, fields) {
3639
3703
  return rest;
3640
3704
  }
3641
3705
 
3642
- // dist/content-store.js
3643
- init_item_keys();
3644
-
3645
3706
  // dist/static/seo.js
3646
3707
  var DEFAULT_SEO_FIELD_NAMES = {
3647
3708
  title: "metaTitle",
@@ -5834,7 +5895,7 @@ var ContentStore = class {
5834
5895
  absolutePath
5835
5896
  };
5836
5897
  }
5837
- doc.data = withItemKeys(fields, doc.data, "content");
5898
+ doc.data = entryAsRead(fields, doc.data);
5838
5899
  if (options.resolveReferences !== false) {
5839
5900
  doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
5840
5901
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -5938,7 +5999,7 @@ var ContentStore = class {
5938
5999
  } else if (input.format === "yaml") {
5939
6000
  content = serializeYaml(input.data ?? {}, existingRaw);
5940
6001
  } else {
5941
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
6002
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
5942
6003
  }
5943
6004
  await atomicWriteFile(absolutePath, content);
5944
6005
  const liveIndex = this._idIndex;
@@ -6830,6 +6891,7 @@ _${field.description}_` : "";
6830
6891
  switch (field.type) {
6831
6892
  case "string":
6832
6893
  case "number":
6894
+ case "date":
6833
6895
  case "datetime":
6834
6896
  return `${heading} ${label}${descriptionLine}
6835
6897
 
@@ -6908,6 +6970,7 @@ ${items.map((item) => `- ${item}`).join("\n")}`;
6908
6970
  var FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
6909
6971
  "string",
6910
6972
  "number",
6973
+ "date",
6911
6974
  "datetime",
6912
6975
  "boolean",
6913
6976
  "select",
@@ -7845,6 +7908,7 @@ var branchMetadataShape = {
7845
7908
  merged: "merged"
7846
7909
  }).optional(),
7847
7910
  mergedAt: z7.string().optional(),
7911
+ archivedAt: z7.string().optional(),
7848
7912
  rebaseFailure: z7.object({ message: z7.string(), firstAt: z7.string(), lastAt: z7.string() }).optional(),
7849
7913
  historyRewrittenFrom: z7.string().optional(),
7850
7914
  lastGitHubTip: z7.string().optional(),
@@ -10970,7 +11034,6 @@ import path24 from "node:path";
10970
11034
  import matter3 from "gray-matter";
10971
11035
  import { parse as yamlParse2 } from "yaml";
10972
11036
  init_error();
10973
- init_item_keys();
10974
11037
  var log10 = createDebugLogger({ prefix: "ContentListing" });
10975
11038
  var normalizePath = (root, target) => {
10976
11039
  const result = validateAndNormalizePath(root, target);
@@ -11141,7 +11204,7 @@ var listCollectionEntries = async (root, collection, onSkip, readsId) => {
11141
11204
  fs20.stat(absolutePath),
11142
11205
  readEntryData(absolutePath, format, bodyField)
11143
11206
  ]);
11144
- const data = withItemKeys(entryType?.schema ?? [], rawData, "content");
11207
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
11145
11208
  const item = {
11146
11209
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
11147
11210
  contentId,
@@ -11254,6 +11317,11 @@ function validateScalar(field, value, path27) {
11254
11317
  return [{ fieldPath: path27, message: "Expected true or false" }];
11255
11318
  }
11256
11319
  return [];
11320
+ case "date":
11321
+ if (value !== "" && !isCalendarDay(value)) {
11322
+ return [{ fieldPath: path27, message: "Expected a real date, written YYYY-MM-DD" }];
11323
+ }
11324
+ return [];
11257
11325
  case "datetime":
11258
11326
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
11259
11327
  return [{ fieldPath: path27, message: "Expected a valid date/time string" }];
@@ -11408,7 +11476,7 @@ function findInvalidEntries(items) {
11408
11476
  for (const item of items) {
11409
11477
  if (!item.schema)
11410
11478
  continue;
11411
- const data = normalizeDatesDeep(item.data);
11479
+ const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
11412
11480
  const errors = validateEntryData(item.schema, data);
11413
11481
  if (errors.length > 0) {
11414
11482
  invalid.push({ entryPath: item.entryPath, errors });
package/dist/cli/init.js CHANGED
@@ -26,7 +26,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
26
26
  var init_types = __esm({
27
27
  "dist/config/types.js"() {
28
28
  "use strict";
29
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
29
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
30
30
  markdownFieldTypes = ["markdown", "mdx"];
31
31
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
32
32
  fieldTypes = [
@@ -10,10 +10,10 @@ import type { PermissionPath } from '../authorization/types.js';
10
10
  import type { EntryLinkUrlResolver } from '../entry-link-resolver.js';
11
11
  import type { CropRect } from '../assets/transform-directives.js';
12
12
  import type { SeoFieldLocation } from '../static/seo.js';
13
- declare const scalarFieldTypes: readonly ["string", "number", "boolean", "datetime", "code"];
13
+ declare const scalarFieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code"];
14
14
  export declare const markdownFieldTypes: readonly ["markdown", "mdx"];
15
- export declare const primitiveFieldTypes: readonly ["string", "number", "boolean", "datetime", "code", "markdown", "mdx"];
16
- export declare const fieldTypes: readonly ["string", "number", "boolean", "datetime", "code", "markdown", "mdx", "image", "select", "reference", "object", "block", "group"];
15
+ export declare const primitiveFieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code", "markdown", "mdx"];
16
+ export declare const fieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code", "markdown", "mdx", "image", "select", "reference", "object", "block", "group"];
17
17
  export type PrimitiveFieldType = (typeof primitiveFieldTypes)[number];
18
18
  export type FieldType = (typeof fieldTypes)[number];
19
19
  export type ContentFormat = 'md' | 'mdx' | 'json' | 'yaml';
@@ -211,7 +211,7 @@ export interface ObjectFieldConfig extends BaseFieldConfig {
211
211
  type: 'object';
212
212
  fields: FieldConfig[];
213
213
  /**
214
- * On a `list: true` object, a direct `string` or `number` child whose value titles each
214
+ * On a `list: true` object, a direct `string`, `number` or `date` child whose value titles each
215
215
  * item's card; a blank or non-finite value falls back to the item's first non-blank `string`
216
216
  * field, then "Item N".
217
217
  */
@@ -2,7 +2,7 @@
2
2
  * Type definitions for CanopyCMS configuration.
3
3
  * These are pure TypeScript types - Zod schemas are in ./schemas/
4
4
  */
5
- const scalarFieldTypes = ['string', 'number', 'boolean', 'datetime', 'code'];
5
+ const scalarFieldTypes = ['string', 'number', 'boolean', 'date', 'datetime', 'code'];
6
6
  export const markdownFieldTypes = ['markdown', 'mdx'];
7
7
  export const primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
8
8
  // 'image' is not a primitive: it has a structured value (ImageFieldValue) and
@@ -34,9 +34,9 @@ export declare const ensureReferenceFieldsHaveScope: (fields: unknown) => void;
34
34
  export declare const forEachReferenceField: (fields: unknown, visit: (field: Record<string, unknown>) => void) => void;
35
35
  /**
36
36
  * Check that every `itemLabel` is non-blank text and, like every `itemTitleField`, sits on a
37
- * `list: true` object, and that every `itemTitleField` names a direct child of type `string` or
38
- * `number`, the only values the card heading shows as typed (a reference would show its raw id, a
39
- * select its stored value). Throws an error when it does not. Entry schemas reach the editor
37
+ * `list: true` object, and that every `itemTitleField` names a direct child of type `string`,
38
+ * `number` or `date`, the only values the card heading shows as typed (a reference would show its
39
+ * raw id, a select its stored value). Throws an error when it does not. Entry schemas reach the editor
40
40
  * through these checks, not the zod field schema.
41
41
  */
42
42
  export declare const ensureItemTitleFieldsExist: (fields: unknown) => void;
@@ -68,6 +68,12 @@ export declare const ensureStampOnPublishPlacement: (fields: unknown, scopeLabel
68
68
  * `ensureStampOnPublishPlacement`. Throws on the first problem.
69
69
  */
70
70
  export declare const ensureMaxItemsPlacement: (fields: unknown, scopeLabel?: string) => void;
71
+ /**
72
+ * Check that no `date` field is `list: true`: the editor has one picker for one day, and an object
73
+ * list with a `date` child gives a list of days its rows. Recurses into groups, objects and block
74
+ * templates like `ensureStampOnPublishPlacement`. Throws on the first problem.
75
+ */
76
+ export declare const ensureSingleDateFields: (fields: unknown, scopeLabel?: string) => void;
71
77
  /**
72
78
  * Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
73
79
  * `mdxAllow` and `executable` against each other. Throws on the first problem.
@@ -110,9 +110,9 @@ export const forEachReferenceField = (fields, visit) => {
110
110
  };
111
111
  /**
112
112
  * Check that every `itemLabel` is non-blank text and, like every `itemTitleField`, sits on a
113
- * `list: true` object, and that every `itemTitleField` names a direct child of type `string` or
114
- * `number`, the only values the card heading shows as typed (a reference would show its raw id, a
115
- * select its stored value). Throws an error when it does not. Entry schemas reach the editor
113
+ * `list: true` object, and that every `itemTitleField` names a direct child of type `string`,
114
+ * `number` or `date`, the only values the card heading shows as typed (a reference would show its
115
+ * raw id, a select its stored value). Throws an error when it does not. Entry schemas reach the editor
116
116
  * through these checks, not the zod field schema.
117
117
  */
118
118
  export const ensureItemTitleFieldsExist = (fields) => {
@@ -140,8 +140,8 @@ export const ensureItemTitleFieldsExist = (fields) => {
140
140
  if (typeof f.itemTitleField !== 'string' || !child) {
141
141
  throw new Error(`${label}, which is not one of its child fields`);
142
142
  }
143
- if (child.type !== 'string' && child.type !== 'number') {
144
- throw new Error(`${label}, which must name a string or number field`);
143
+ if (child.type !== 'string' && child.type !== 'number' && child.type !== 'date') {
144
+ throw new Error(`${label}, which must name a string, number or date field`);
145
145
  }
146
146
  }
147
147
  ensureItemTitleFieldsExist(children);
@@ -327,6 +327,31 @@ export const ensureMaxItemsPlacement = (fields, scopeLabel = 'entry schema') =>
327
327
  };
328
328
  check(fields);
329
329
  };
330
+ /**
331
+ * Check that no `date` field is `list: true`: the editor has one picker for one day, and an object
332
+ * list with a `date` child gives a list of days its rows. Recurses into groups, objects and block
333
+ * templates like `ensureStampOnPublishPlacement`. Throws on the first problem.
334
+ */
335
+ export const ensureSingleDateFields = (fields, scopeLabel = 'entry schema') => {
336
+ const check = (scopeFields) => {
337
+ if (!Array.isArray(scopeFields))
338
+ return;
339
+ for (const field of scopeFields) {
340
+ const f = field;
341
+ if (f?.type === 'date' && f.list === true) {
342
+ throw new Error(`${scopeLabel}: field "${String(f.name)}" is a date with list: true; use an object list with a date field inside`);
343
+ }
344
+ if (f?.type === 'group' || f?.type === 'object')
345
+ check(f.fields);
346
+ if (f?.type === 'block' && Array.isArray(f.templates)) {
347
+ for (const template of f.templates) {
348
+ check(template.fields);
349
+ }
350
+ }
351
+ }
352
+ };
353
+ check(fields);
354
+ };
330
355
  /**
331
356
  * Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
332
357
  * `mdxAllow` and `executable` against each other. Throws on the first problem.
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import matter from 'gray-matter';
7
7
  import { parse as yamlParse } from 'yaml';
8
8
  import { findBodyFieldName } from './utils/body-field.js';
9
+ import { entryAsRead } from './utils/calendar-day.js';
9
10
  import { computeEntryUrl } from './utils/entry-url.js';
10
11
  import { asRecord, getFormatExtension } from './utils/format.js';
11
12
  import { resolveCollectionPath, toLogicalCollectionPath } from './content-id-index.js';
@@ -15,7 +16,6 @@ import { isNotFoundError, getErrorMessage } from './utils/error.js';
15
16
  import { createDebugLogger } from './utils/debug.js';
16
17
  import { ContentStore, ContentStoreError, createReferenceResolveCache, } from './content-store.js';
17
18
  import { isBuildMode } from './build-mode.js';
18
- import { withItemKeys } from './item-keys.js';
19
19
  const log = createDebugLogger({ prefix: 'ContentListing' });
20
20
  /** Validate and normalize `target` under `root`; throws ContentStoreError on a traversal. */
21
21
  const normalizePath = (root, target) => {
@@ -444,8 +444,7 @@ export const listCollectionEntries = async (root, collection, onSkip, readsId) =
444
444
  fs.stat(absolutePath),
445
445
  readEntryData(absolutePath, format, bodyField),
446
446
  ]);
447
- // Keyed as `ContentStore.read` keys it, so every reader names an item alike.
448
- const data = withItemKeys(entryType?.schema ?? [], rawData, 'content');
447
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
449
448
  const item = {
450
449
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
451
450
  contentId,
@@ -21,7 +21,7 @@ import { serializeJson } from './utils/json-source-splice.js';
21
21
  import { withLock } from './utils/async-mutex.js';
22
22
  import { assertContentWriteLockHeld, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
23
23
  import { findBodyFieldName, linkOnlyReferenceData } from './utils/body-field.js';
24
- import { withItemKeys } from './item-keys.js';
24
+ import { entryAsRead } from './utils/calendar-day.js';
25
25
  import { buildMissingReference, buildResolvedReference, buildRestrictedReference, } from './entry-schema.js';
26
26
  import { warnDanglingReference } from './dangling-reference-log.js';
27
27
  import { canopyLogError } from './utils/logger.js';
@@ -786,9 +786,9 @@ export class ContentStore {
786
786
  absolutePath,
787
787
  };
788
788
  }
789
- // Keys an item saved without one by its content, before references resolve into it, as the
790
- // listing and the write path do (`item-keys.ts`).
791
- doc.data = withItemKeys(fields, doc.data, 'content');
789
+ // Before references resolve into it. An item saved without a key is keyed by its content, as
790
+ // the listing and the write path do (`item-keys.ts`).
791
+ doc.data = entryAsRead(fields, doc.data);
792
792
  if (options.resolveReferences !== false) {
793
793
  doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess, {
794
794
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -1031,7 +1031,7 @@ export class ContentStore {
1031
1031
  content = serializeYaml(input.data ?? {}, existingRaw);
1032
1032
  }
1033
1033
  else {
1034
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
1034
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
1035
1035
  }
1036
1036
  await atomicWriteFile(absolutePath, content);
1037
1037
  // Update the ID index after a successful write. Look up and mutate the LIVE index
@@ -9,8 +9,11 @@ export interface BranchSummary {
9
9
  name: string;
10
10
  /** What the card shows for the branch (`displayBranchName`); the full `name` keys it. */
11
11
  shortName?: string;
12
+ title?: string;
12
13
  status: string;
13
14
  createdBy?: string;
15
+ /** The creator's display name, which the filter searches. */
16
+ ownerName?: string;
14
17
  updatedAt?: string;
15
18
  access?: {
16
19
  users?: string[];
@@ -99,6 +102,8 @@ export interface BranchManagerProps {
99
102
  onWithdraw?: (name: string) => void;
100
103
  onApprove?: (name: string) => void;
101
104
  onRequestChanges?: (name: string) => void;
105
+ /** Archives a branch whose pull request closed without merging; offered to admins only. */
106
+ onArchive?: (name: string) => void;
102
107
  onClose?: () => void;
103
108
  comments?: CommentThread[];
104
109
  currentUserId?: string;
@@ -114,4 +119,19 @@ export interface BranchManagerProps {
114
119
  /** Names the people in branch comments; without it they show as ids */
115
120
  onGetCommentUserMetadata?: (userId: string) => Promise<UserSearchResult | null>;
116
121
  }
122
+ /**
123
+ * The list's two parts: the current branch, whatever its status, then live branches in `places`;
124
+ * and the other archived branches in `places`.
125
+ * @internal Exported for tests.
126
+ */
127
+ export declare const arrangeBranches: (branches: readonly BranchSummary[], currentBranchName: string | undefined, places: ReadonlyMap<string, number>) => {
128
+ live: BranchSummary[];
129
+ archived: BranchSummary[];
130
+ };
131
+ /**
132
+ * Whether `query` appears in what the card shows of the branch: its name, title or owner's name,
133
+ * ignoring case.
134
+ * @internal Exported for tests.
135
+ */
136
+ export declare const matchesBranchFilter: (branch: BranchSummary, query: string) => boolean;
117
137
  export declare const BranchManager: React.FC<BranchManagerProps>;