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.
- package/CHANGELOG.md +73 -47
- package/dist/ai/json-to-markdown.js +2 -0
- package/dist/api/admin.d.ts +11 -0
- package/dist/api/admin.js +2 -0
- package/dist/api/archived-branches.d.ts +70 -0
- package/dist/api/archived-branches.js +146 -0
- package/dist/api/branch-entry-changes.js +2 -4
- package/dist/api/branch-merge.js +16 -1
- package/dist/api/branch-status.d.ts +11 -0
- package/dist/api/branch-status.js +2 -0
- package/dist/api/branch-withdraw.js +1 -1
- package/dist/api/branch.js +46 -108
- package/dist/api/client.d.ts +25 -21
- package/dist/api/client.js +46 -38
- package/dist/branch-archive.d.ts +19 -0
- package/dist/branch-archive.js +27 -0
- package/dist/branch-delete.d.ts +45 -0
- package/dist/branch-delete.js +97 -0
- package/dist/branch-metadata-file.d.ts +3 -0
- package/dist/branch-metadata-file.js +1 -0
- package/dist/branch-metadata.d.ts +6 -1
- package/dist/branch-metadata.js +15 -2
- package/dist/cli/cli.js +85 -16
- package/dist/cli/generate-ai-content.js +83 -15
- package/dist/cli/init.js +1 -1
- package/dist/config/types.d.ts +4 -4
- package/dist/config/types.js +1 -1
- package/dist/config/validation.d.ts +9 -3
- package/dist/config/validation.js +30 -5
- package/dist/content-listing.js +2 -3
- package/dist/content-store.js +5 -5
- package/dist/editor/BranchManager.d.ts +20 -0
- package/dist/editor/BranchManager.js +92 -26
- package/dist/editor/Editor.js +5 -1
- package/dist/editor/FormRenderer.js +5 -0
- package/dist/editor/admin/SystemHealthPanel.js +61 -6
- package/dist/editor/admin/useSystemHealth.d.ts +3 -0
- package/dist/editor/admin/useSystemHealth.js +59 -0
- package/dist/editor/branch-status.js +9 -0
- package/dist/editor/components/EntryChangesDrawer.js +3 -2
- package/dist/editor/copy.d.ts +3 -0
- package/dist/editor/copy.js +3 -0
- package/dist/editor/entry-diff.js +7 -1
- package/dist/editor/fields/DateField.d.ts +19 -0
- package/dist/editor/fields/DateField.js +53 -0
- package/dist/editor/fields/DateTimeField.d.ts +10 -0
- package/dist/editor/fields/DateTimeField.js +5 -15
- package/dist/editor/fields/list-item-title.js +2 -1
- package/dist/editor/hooks/useBranchManager.d.ts +5 -0
- package/dist/editor/hooks/useBranchManager.js +55 -4
- package/dist/editor/hooks/useBranchesData.d.ts +2 -0
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/theme.js +12 -4
- package/dist/entry-schema-registry.js +2 -1
- package/dist/static/index.js +5 -2
- package/dist/types.d.ts +33 -0
- package/dist/utils/calendar-day.d.ts +23 -0
- package/dist/utils/calendar-day.js +102 -0
- package/dist/utils/content-serialize.d.ts +4 -2
- package/dist/utils/content-serialize.js +8 -6
- package/dist/validation/entry-validator.js +7 -1
- package/dist/validation/field-traversal.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/archived-sweep.d.ts +23 -0
- package/dist/worker/archived-sweep.js +137 -0
- package/dist/worker/cms-worker.d.ts +6 -0
- package/dist/worker/cms-worker.js +7 -0
- package/dist/worker/git-sync.d.ts +4 -4
- package/dist/worker/git-sync.js +3 -0
- package/dist/worker/github-commits.d.ts +2 -2
- package/dist/worker/github-commits.js +5 -2
- package/dist/worker/github-gateway-wire.d.ts +2 -2
- package/dist/worker/rebase.js +6 -2
- package/dist/worker/task-runner.js +7 -4
- package/dist/worker/worker-context.d.ts +2 -0
- 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 =
|
|
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 =
|
|
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 = [
|
package/dist/config/types.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
*/
|
package/dist/config/types.js
CHANGED
|
@@ -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
|
|
38
|
-
* `number`, the only values the card heading shows as typed (a reference would show its
|
|
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
|
|
114
|
-
* `number`, the only values the card heading shows as typed (a reference would show its
|
|
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
|
|
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.
|
package/dist/content-listing.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/content-store.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
790
|
-
// listing and the write path do (`item-keys.ts`).
|
|
791
|
-
doc.data =
|
|
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>;
|