canopycms 0.4.0 → 0.5.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 +66 -42
- package/dist/ai/generate.d.ts +3 -0
- package/dist/ai/generate.js +12 -29
- package/dist/ai/handler.js +1 -0
- package/dist/ai/types.d.ts +3 -9
- package/dist/api/reference-options.d.ts +2 -5
- package/dist/api/reference-options.js +21 -2
- package/dist/api/schema.d.ts +4 -4
- package/dist/build/generate-ai-content.js +1 -0
- package/dist/cli/cli.js +131 -38
- package/dist/cli/generate-ai-content.js +75 -32
- package/dist/cli/init.js +29 -4
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +74 -0
- package/dist/config/schemas/config.js +21 -0
- package/dist/config/schemas/field.d.ts +30 -0
- package/dist/config/schemas/field.js +22 -0
- package/dist/config/types.d.ts +39 -0
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +33 -0
- package/dist/editor/FormRenderer.js +15 -13
- package/dist/editor/copy.d.ts +5 -0
- package/dist/editor/copy.js +5 -0
- package/dist/editor/fields/BlockField.d.ts +2 -0
- package/dist/editor/fields/BlockField.js +15 -8
- package/dist/editor/fields/ItemList.d.ts +21 -3
- package/dist/editor/fields/ItemList.js +16 -8
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +6 -2
- package/dist/editor/fields/ObjectListField.js +9 -3
- package/dist/editor/fields/ReferenceField.d.ts +6 -7
- package/dist/editor/fields/ReferenceField.js +28 -122
- package/dist/editor/fields/ReferenceListField.d.ts +41 -0
- package/dist/editor/fields/ReferenceListField.js +139 -0
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +6 -3
- package/dist/editor/fields/StringListField.d.ts +10 -1
- package/dist/editor/fields/StringListField.js +18 -8
- package/dist/editor/fields/item-expansion.d.ts +7 -3
- package/dist/editor/fields/item-expansion.js +6 -4
- package/dist/editor/fields/list-item-keys.d.ts +6 -5
- package/dist/editor/fields/list-item-keys.js +45 -13
- package/dist/editor/fields/reference-noindex.d.ts +37 -0
- package/dist/editor/fields/reference-noindex.js +48 -0
- package/dist/editor/fields/use-reference-options.d.ts +36 -0
- package/dist/editor/fields/use-reference-options.js +121 -0
- package/dist/editor/form-outline.d.ts +2 -2
- package/dist/editor/form-outline.js +4 -2
- package/dist/editor/theme.js +5 -0
- package/dist/entry-schema-registry.d.ts +1 -1
- package/dist/entry-schema-registry.js +3 -2
- package/dist/entry-schema.d.ts +5 -3
- package/dist/entry-schema.js +2 -1
- package/dist/reference-resolver.d.ts +18 -1
- package/dist/reference-resolver.js +175 -8
- package/dist/schema/schema-store.d.ts +2 -2
- package/dist/static/index.d.ts +8 -4
- package/dist/static/index.js +38 -13
- package/dist/static/seo.d.ts +7 -5
- package/dist/static/seo.js +13 -6
- package/dist/validation/entry-validator.d.ts +5 -0
- package/dist/validation/entry-validator.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli/init.js
CHANGED
|
@@ -160,7 +160,7 @@ var init_mdx_allowlist = __esm({
|
|
|
160
160
|
|
|
161
161
|
// dist/config/schemas/field.js
|
|
162
162
|
import { z } from "zod";
|
|
163
|
-
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
163
|
+
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
164
164
|
var init_field = __esm({
|
|
165
165
|
"dist/config/schemas/field.js"() {
|
|
166
166
|
"use strict";
|
|
@@ -173,7 +173,10 @@ var init_field = __esm({
|
|
|
173
173
|
required: z.boolean().optional(),
|
|
174
174
|
list: z.boolean().optional(),
|
|
175
175
|
isTitle: z.boolean().optional(),
|
|
176
|
-
isBody: z.boolean().optional()
|
|
176
|
+
isBody: z.boolean().optional(),
|
|
177
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
178
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
179
|
+
maxItems: z.number().int().positive().optional()
|
|
177
180
|
});
|
|
178
181
|
selectOptionSchema = z.union([
|
|
179
182
|
z.string(),
|
|
@@ -231,6 +234,7 @@ var init_field = __esm({
|
|
|
231
234
|
type: z.literal("select"),
|
|
232
235
|
options: z.array(selectOptionSchema).min(1)
|
|
233
236
|
});
|
|
237
|
+
referenceSortFieldSchema = z.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
234
238
|
referenceFieldSchema = fieldBaseSchema.extend({
|
|
235
239
|
type: z.literal("reference"),
|
|
236
240
|
collections: z.array(z.string().min(1)).min(1).optional(),
|
|
@@ -241,7 +245,14 @@ var init_field = __esm({
|
|
|
241
245
|
// default, so a consumer that adopts this schema's parse output would silently delete a
|
|
242
246
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
243
247
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
244
|
-
includeBody: z.boolean().optional()
|
|
248
|
+
includeBody: z.boolean().optional(),
|
|
249
|
+
optionSort: z.union([
|
|
250
|
+
z.enum(["label", "collection"]),
|
|
251
|
+
z.object({
|
|
252
|
+
field: referenceSortFieldSchema,
|
|
253
|
+
direction: z.enum(["asc", "desc"]).optional()
|
|
254
|
+
}).strict()
|
|
255
|
+
]).optional()
|
|
245
256
|
});
|
|
246
257
|
fieldHolder = [z.never()];
|
|
247
258
|
blockSchema = z.object({
|
|
@@ -439,7 +450,7 @@ var init_media = __esm({
|
|
|
439
450
|
|
|
440
451
|
// dist/config/schemas/config.js
|
|
441
452
|
import { z as z5 } from "zod";
|
|
442
|
-
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
453
|
+
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
443
454
|
var init_config = __esm({
|
|
444
455
|
"dist/config/schemas/config.js"() {
|
|
445
456
|
"use strict";
|
|
@@ -486,6 +497,19 @@ var init_config = __esm({
|
|
|
486
497
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
487
498
|
SignInComponent: z5.custom().optional()
|
|
488
499
|
});
|
|
500
|
+
seoFieldNameSchema = z5.string().min(1).optional();
|
|
501
|
+
seoSchema = z5.object({
|
|
502
|
+
group: z5.string().min(1).optional(),
|
|
503
|
+
fields: z5.object({
|
|
504
|
+
title: seoFieldNameSchema,
|
|
505
|
+
description: seoFieldNameSchema,
|
|
506
|
+
ogImage: seoFieldNameSchema,
|
|
507
|
+
ogType: seoFieldNameSchema,
|
|
508
|
+
canonical: seoFieldNameSchema,
|
|
509
|
+
noindex: seoFieldNameSchema,
|
|
510
|
+
twitterCard: seoFieldNameSchema
|
|
511
|
+
}).strict().optional()
|
|
512
|
+
}).strict();
|
|
489
513
|
CanopyConfigSchema = z5.object({
|
|
490
514
|
media: mediaSchema.optional(),
|
|
491
515
|
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
@@ -535,6 +559,7 @@ var init_config = __esm({
|
|
|
535
559
|
entryLinkUrl: z5.custom().optional(),
|
|
536
560
|
validateEntry: z5.custom().optional(),
|
|
537
561
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
562
|
+
seo: seoSchema.optional(),
|
|
538
563
|
dev: devConfigSchema.optional()
|
|
539
564
|
}).strict();
|
|
540
565
|
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
package/dist/config/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { defineCanopyConfig, type FieldConfig } from 'canopycms'
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
11
|
+
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ReferenceOptionSort, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
12
12
|
export { primitiveFieldTypes, fieldTypes } from './types.js';
|
|
13
13
|
export { CanopyConfigSchema, getConfigDefaults, DEFAULT_PROD_WORKSPACE } from './schemas/config.js';
|
|
14
14
|
export { fieldSchema, blockSchema, selectOptionSchema, referenceOptionSchema, imageFieldSchema, } from './schemas/field.js';
|
|
@@ -105,6 +105,56 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
105
105
|
entryLinkUrl: z.ZodOptional<z.ZodType<EntryLinkUrlResolver, z.ZodTypeDef, EntryLinkUrlResolver>>;
|
|
106
106
|
validateEntry: z.ZodOptional<z.ZodType<ValidateEntryHook, z.ZodTypeDef, ValidateEntryHook>>;
|
|
107
107
|
mdxAllow: z.ZodOptional<z.ZodType<import("../index.js").MdxAllowlist, z.ZodTypeDef, import("../index.js").MdxAllowlist>>;
|
|
108
|
+
seo: z.ZodOptional<z.ZodObject<{
|
|
109
|
+
group: z.ZodOptional<z.ZodString>;
|
|
110
|
+
fields: z.ZodOptional<z.ZodObject<{
|
|
111
|
+
title: z.ZodOptional<z.ZodString>;
|
|
112
|
+
description: z.ZodOptional<z.ZodString>;
|
|
113
|
+
ogImage: z.ZodOptional<z.ZodString>;
|
|
114
|
+
ogType: z.ZodOptional<z.ZodString>;
|
|
115
|
+
canonical: z.ZodOptional<z.ZodString>;
|
|
116
|
+
noindex: z.ZodOptional<z.ZodString>;
|
|
117
|
+
twitterCard: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, "strict", z.ZodTypeAny, {
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
title?: string | undefined;
|
|
121
|
+
ogImage?: string | undefined;
|
|
122
|
+
ogType?: string | undefined;
|
|
123
|
+
canonical?: string | undefined;
|
|
124
|
+
noindex?: string | undefined;
|
|
125
|
+
twitterCard?: string | undefined;
|
|
126
|
+
}, {
|
|
127
|
+
description?: string | undefined;
|
|
128
|
+
title?: string | undefined;
|
|
129
|
+
ogImage?: string | undefined;
|
|
130
|
+
ogType?: string | undefined;
|
|
131
|
+
canonical?: string | undefined;
|
|
132
|
+
noindex?: string | undefined;
|
|
133
|
+
twitterCard?: string | undefined;
|
|
134
|
+
}>>;
|
|
135
|
+
}, "strict", z.ZodTypeAny, {
|
|
136
|
+
fields?: {
|
|
137
|
+
description?: string | undefined;
|
|
138
|
+
title?: string | undefined;
|
|
139
|
+
ogImage?: string | undefined;
|
|
140
|
+
ogType?: string | undefined;
|
|
141
|
+
canonical?: string | undefined;
|
|
142
|
+
noindex?: string | undefined;
|
|
143
|
+
twitterCard?: string | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
group?: string | undefined;
|
|
146
|
+
}, {
|
|
147
|
+
fields?: {
|
|
148
|
+
description?: string | undefined;
|
|
149
|
+
title?: string | undefined;
|
|
150
|
+
ogImage?: string | undefined;
|
|
151
|
+
ogType?: string | undefined;
|
|
152
|
+
canonical?: string | undefined;
|
|
153
|
+
noindex?: string | undefined;
|
|
154
|
+
twitterCard?: string | undefined;
|
|
155
|
+
} | undefined;
|
|
156
|
+
group?: string | undefined;
|
|
157
|
+
}>>;
|
|
108
158
|
dev: z.ZodOptional<z.ZodObject<{
|
|
109
159
|
contentSync: z.ZodOptional<z.ZodDefault<z.ZodEnum<["off", "warn"]>>>;
|
|
110
160
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -167,6 +217,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
167
217
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
168
218
|
validateEntry?: ValidateEntryHook | undefined;
|
|
169
219
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
220
|
+
seo?: {
|
|
221
|
+
fields?: {
|
|
222
|
+
description?: string | undefined;
|
|
223
|
+
title?: string | undefined;
|
|
224
|
+
ogImage?: string | undefined;
|
|
225
|
+
ogType?: string | undefined;
|
|
226
|
+
canonical?: string | undefined;
|
|
227
|
+
noindex?: string | undefined;
|
|
228
|
+
twitterCard?: string | undefined;
|
|
229
|
+
} | undefined;
|
|
230
|
+
group?: string | undefined;
|
|
231
|
+
} | undefined;
|
|
170
232
|
}, {
|
|
171
233
|
gitBotAuthorName: string;
|
|
172
234
|
gitBotAuthorEmail: string;
|
|
@@ -222,6 +284,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
222
284
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
223
285
|
validateEntry?: ValidateEntryHook | undefined;
|
|
224
286
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
287
|
+
seo?: {
|
|
288
|
+
fields?: {
|
|
289
|
+
description?: string | undefined;
|
|
290
|
+
title?: string | undefined;
|
|
291
|
+
ogImage?: string | undefined;
|
|
292
|
+
ogType?: string | undefined;
|
|
293
|
+
canonical?: string | undefined;
|
|
294
|
+
noindex?: string | undefined;
|
|
295
|
+
twitterCard?: string | undefined;
|
|
296
|
+
} | undefined;
|
|
297
|
+
group?: string | undefined;
|
|
298
|
+
} | undefined;
|
|
225
299
|
}>;
|
|
226
300
|
/**
|
|
227
301
|
* Default workspace path for prod mode (used when CANOPYCMS_WORKSPACE_ROOT is not set): where
|
|
@@ -61,6 +61,26 @@ const editorConfigSchema = z.object({
|
|
|
61
61
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
62
62
|
SignInComponent: z.custom().optional(),
|
|
63
63
|
});
|
|
64
|
+
const seoFieldNameSchema = z.string().min(1).optional();
|
|
65
|
+
// Strict, so a misspelled key (`noIndex`) fails the parse instead of leaving every surface
|
|
66
|
+
// reading the default field name.
|
|
67
|
+
const seoSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
group: z.string().min(1).optional(),
|
|
70
|
+
fields: z
|
|
71
|
+
.object({
|
|
72
|
+
title: seoFieldNameSchema,
|
|
73
|
+
description: seoFieldNameSchema,
|
|
74
|
+
ogImage: seoFieldNameSchema,
|
|
75
|
+
ogType: seoFieldNameSchema,
|
|
76
|
+
canonical: seoFieldNameSchema,
|
|
77
|
+
noindex: seoFieldNameSchema,
|
|
78
|
+
twitterCard: seoFieldNameSchema,
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.optional(),
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
64
84
|
export const CanopyConfigSchema = z
|
|
65
85
|
.object({
|
|
66
86
|
media: mediaSchema.optional(),
|
|
@@ -110,6 +130,7 @@ export const CanopyConfigSchema = z
|
|
|
110
130
|
entryLinkUrl: z.custom().optional(),
|
|
111
131
|
validateEntry: z.custom().optional(),
|
|
112
132
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
133
|
+
seo: seoSchema.optional(),
|
|
113
134
|
dev: devConfigSchema.optional(),
|
|
114
135
|
})
|
|
115
136
|
.strict();
|
|
@@ -33,6 +33,7 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
33
33
|
list: z.ZodOptional<z.ZodBoolean>;
|
|
34
34
|
isTitle: z.ZodOptional<z.ZodBoolean>;
|
|
35
35
|
isBody: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
maxItems: z.ZodOptional<z.ZodNumber>;
|
|
36
37
|
} & {
|
|
37
38
|
type: z.ZodLiteral<"image">;
|
|
38
39
|
aspect: z.ZodOptional<z.ZodString>;
|
|
@@ -46,6 +47,7 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
46
47
|
list?: boolean | undefined;
|
|
47
48
|
isTitle?: boolean | undefined;
|
|
48
49
|
isBody?: boolean | undefined;
|
|
50
|
+
maxItems?: number | undefined;
|
|
49
51
|
aspect?: string | undefined;
|
|
50
52
|
altOptional?: boolean | undefined;
|
|
51
53
|
}, {
|
|
@@ -57,9 +59,16 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
57
59
|
list?: boolean | undefined;
|
|
58
60
|
isTitle?: boolean | undefined;
|
|
59
61
|
isBody?: boolean | undefined;
|
|
62
|
+
maxItems?: number | undefined;
|
|
60
63
|
aspect?: string | undefined;
|
|
61
64
|
altOptional?: boolean | undefined;
|
|
62
65
|
}>;
|
|
66
|
+
/**
|
|
67
|
+
* A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
|
|
68
|
+
* fail at config time rather than sorting on a value no entry has.
|
|
69
|
+
* @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
|
|
70
|
+
*/
|
|
71
|
+
export declare const referenceSortFieldSchema: z.ZodString;
|
|
63
72
|
/** @internal Exported for tests. */
|
|
64
73
|
export declare const referenceFieldSchema: z.ZodObject<{
|
|
65
74
|
name: z.ZodString;
|
|
@@ -69,6 +78,7 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
69
78
|
list: z.ZodOptional<z.ZodBoolean>;
|
|
70
79
|
isTitle: z.ZodOptional<z.ZodBoolean>;
|
|
71
80
|
isBody: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
maxItems: z.ZodOptional<z.ZodNumber>;
|
|
72
82
|
} & {
|
|
73
83
|
type: z.ZodLiteral<"reference">;
|
|
74
84
|
collections: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -85,6 +95,16 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
85
95
|
label: string;
|
|
86
96
|
}>]>, "many">>;
|
|
87
97
|
includeBody: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
optionSort: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["label", "collection"]>, z.ZodObject<{
|
|
99
|
+
field: z.ZodString;
|
|
100
|
+
direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
|
101
|
+
}, "strict", z.ZodTypeAny, {
|
|
102
|
+
field: string;
|
|
103
|
+
direction?: "asc" | "desc" | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
field: string;
|
|
106
|
+
direction?: "asc" | "desc" | undefined;
|
|
107
|
+
}>]>>;
|
|
88
108
|
}, "strip", z.ZodTypeAny, {
|
|
89
109
|
name: string;
|
|
90
110
|
type: "reference";
|
|
@@ -98,10 +118,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
98
118
|
list?: boolean | undefined;
|
|
99
119
|
isTitle?: boolean | undefined;
|
|
100
120
|
isBody?: boolean | undefined;
|
|
121
|
+
maxItems?: number | undefined;
|
|
101
122
|
collections?: string[] | undefined;
|
|
102
123
|
entryTypes?: string[] | undefined;
|
|
103
124
|
displayField?: string | undefined;
|
|
104
125
|
includeBody?: boolean | undefined;
|
|
126
|
+
optionSort?: "collection" | "label" | {
|
|
127
|
+
field: string;
|
|
128
|
+
direction?: "asc" | "desc" | undefined;
|
|
129
|
+
} | undefined;
|
|
105
130
|
}, {
|
|
106
131
|
name: string;
|
|
107
132
|
type: "reference";
|
|
@@ -115,10 +140,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
115
140
|
list?: boolean | undefined;
|
|
116
141
|
isTitle?: boolean | undefined;
|
|
117
142
|
isBody?: boolean | undefined;
|
|
143
|
+
maxItems?: number | undefined;
|
|
118
144
|
collections?: string[] | undefined;
|
|
119
145
|
entryTypes?: string[] | undefined;
|
|
120
146
|
displayField?: string | undefined;
|
|
121
147
|
includeBody?: boolean | undefined;
|
|
148
|
+
optionSort?: "collection" | "label" | {
|
|
149
|
+
field: string;
|
|
150
|
+
direction?: "asc" | "desc" | undefined;
|
|
151
|
+
} | undefined;
|
|
122
152
|
}>;
|
|
123
153
|
export declare const blockSchema: z.ZodObject<{
|
|
124
154
|
name: z.ZodString;
|
|
@@ -12,6 +12,9 @@ const fieldBaseSchema = z.object({
|
|
|
12
12
|
list: z.boolean().optional(),
|
|
13
13
|
isTitle: z.boolean().optional(),
|
|
14
14
|
isBody: z.boolean().optional(),
|
|
15
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
16
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
17
|
+
maxItems: z.number().int().positive().optional(),
|
|
15
18
|
});
|
|
16
19
|
export const selectOptionSchema = z.union([
|
|
17
20
|
z.string(),
|
|
@@ -94,6 +97,14 @@ const selectFieldSchema = fieldBaseSchema.extend({
|
|
|
94
97
|
type: z.literal('select'),
|
|
95
98
|
options: z.array(selectOptionSchema).min(1),
|
|
96
99
|
});
|
|
100
|
+
/**
|
|
101
|
+
* A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
|
|
102
|
+
* fail at config time rather than sorting on a value no entry has.
|
|
103
|
+
* @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
|
|
104
|
+
*/
|
|
105
|
+
export const referenceSortFieldSchema = z
|
|
106
|
+
.string()
|
|
107
|
+
.regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
97
108
|
// At least one of `collections` or `entryTypes` must be specified (enforced by config validation).
|
|
98
109
|
/** @internal Exported for tests. */
|
|
99
110
|
export const referenceFieldSchema = fieldBaseSchema.extend({
|
|
@@ -107,6 +118,17 @@ export const referenceFieldSchema = fieldBaseSchema.extend({
|
|
|
107
118
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
108
119
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
109
120
|
includeBody: z.boolean().optional(),
|
|
121
|
+
optionSort: z
|
|
122
|
+
.union([
|
|
123
|
+
z.enum(['label', 'collection']),
|
|
124
|
+
z
|
|
125
|
+
.object({
|
|
126
|
+
field: referenceSortFieldSchema,
|
|
127
|
+
direction: z.enum(['asc', 'desc']).optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict(),
|
|
130
|
+
])
|
|
131
|
+
.optional(),
|
|
110
132
|
});
|
|
111
133
|
// Use a mutable holder to enable forward references in recursive z.lazy() closures.
|
|
112
134
|
// blockSchema/objectFieldSchema reference fieldHolder[0] via z.lazy, which is resolved
|
package/dist/config/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { LogicalPath, ContentId } from '../paths/types.js';
|
|
|
9
9
|
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
|
+
import type { SeoFieldLocation } from '../static/seo.js';
|
|
12
13
|
declare const scalarFieldTypes: readonly ["string", "number", "boolean", "datetime", "code"];
|
|
13
14
|
export declare const markdownFieldTypes: readonly ["markdown", "mdx"];
|
|
14
15
|
export declare const primitiveFieldTypes: readonly ["string", "number", "boolean", "datetime", "code", "markdown", "mdx"];
|
|
@@ -52,6 +53,13 @@ interface BaseFieldConfig {
|
|
|
52
53
|
* `includeBody: true`.
|
|
53
54
|
*/
|
|
54
55
|
isBody?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* On a `list: true` field or a `block` field: the most items the list may hold. The editor
|
|
58
|
+
* stops adding at the cap, saving over it is refused, and a static build fails on an entry
|
|
59
|
+
* over it, so adding a cap to a list that already holds more fails the build until the
|
|
60
|
+
* content is fixed. `ensureMaxItemsPlacement` (`validation.ts`) refuses it elsewhere in a registry schema.
|
|
61
|
+
*/
|
|
62
|
+
maxItems?: number;
|
|
55
63
|
}
|
|
56
64
|
interface PrimitiveFieldConfig extends BaseFieldConfig {
|
|
57
65
|
type: (typeof scalarFieldTypes)[number];
|
|
@@ -136,7 +144,29 @@ export interface ReferenceFieldConfig extends BaseFieldConfig {
|
|
|
136
144
|
* to different shapes in different places.
|
|
137
145
|
*/
|
|
138
146
|
includeBody?: boolean;
|
|
147
|
+
/** Order of the picker's options. Default `'label'`. See {@link ReferenceOptionSort}. */
|
|
148
|
+
optionSort?: ReferenceOptionSort;
|
|
139
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* How a reference picker orders its options. Every mode runs after the search filter.
|
|
152
|
+
*
|
|
153
|
+
* - `'label'`: alphabetical by label.
|
|
154
|
+
* - `'collection'`: one group per collection, in content-tree order (a collection, then its
|
|
155
|
+
* subcollections), each in its collection's own order, then by label.
|
|
156
|
+
* - `{ field, direction }`: by the target's value at `field`, a dotted path for a nested field
|
|
157
|
+
* (`'meta.publishedAt'`). Numbers compare as numbers and sort before strings (`'desc'` reverses that too); strings compare
|
|
158
|
+
* as plain text, so point it at an ISO date (`YYYY-MM-DD`; unquoted frontmatter dates count):
|
|
159
|
+
* a display date like `'March 4'` sorts by month name. An entry with no value sorts first
|
|
160
|
+
* under `'desc'`, last under `'asc'` (the default). Ties go by label.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* // Newest first, with entries not yet given a date at the top.
|
|
164
|
+
* optionSort: { field: 'publishedAt', direction: 'desc' }
|
|
165
|
+
*/
|
|
166
|
+
export type ReferenceOptionSort = 'label' | 'collection' | {
|
|
167
|
+
field: string;
|
|
168
|
+
direction?: 'asc' | 'desc';
|
|
169
|
+
};
|
|
140
170
|
/**
|
|
141
171
|
* Image field config. The value is a structured object (see `ImageFieldValue`
|
|
142
172
|
* below), not a bare URL string.
|
|
@@ -460,6 +490,8 @@ export interface CanopyConfig {
|
|
|
460
490
|
validateEntry?: ValidateEntryHook;
|
|
461
491
|
/** Site default for every field checked as MDX — see {@link CanopyConfigInput.mdxAllow}. */
|
|
462
492
|
mdxAllow?: MdxAllowlist;
|
|
493
|
+
/** Where entries keep their SEO fields — see {@link CanopyConfigInput.seo}. */
|
|
494
|
+
seo?: SeoFieldLocation;
|
|
463
495
|
/** Dev-mode-only behavior — see {@link CanopyConfigInput.dev}. */
|
|
464
496
|
dev?: DevConfig;
|
|
465
497
|
}
|
|
@@ -549,6 +581,13 @@ export interface CanopyConfigInput {
|
|
|
549
581
|
validateEntry?: ValidateEntryHook;
|
|
550
582
|
/** What every field checked as MDX accepts; a field's own `mdxAllow` replaces it key by key. */
|
|
551
583
|
mdxAllow?: MdxAllowlist;
|
|
584
|
+
/**
|
|
585
|
+
* Where entries keep their SEO fields: `group` when `defineSeoFieldGroup({ group })` nests
|
|
586
|
+
* them, `fields` for renamed ones. Omit for the flat default. Every surface that reads an
|
|
587
|
+
* entry's `noindex` reads it here: the sitemap and page metadata helpers `createNextCanopyContext`
|
|
588
|
+
* returns, AI content and the editor's reference picker.
|
|
589
|
+
*/
|
|
590
|
+
seo?: SeoFieldLocation;
|
|
552
591
|
/** Dev-mode-only behavior (content-sync divergence detection). Ignored when mode !== 'dev'. */
|
|
553
592
|
dev?: DevConfig;
|
|
554
593
|
}
|
|
@@ -62,6 +62,12 @@ export declare const ensureNoGroupsInsideComplexFields: (fields: unknown) => voi
|
|
|
62
62
|
* never be saved empty, so it would never be stamped. Throws on the first problem.
|
|
63
63
|
*/
|
|
64
64
|
export declare const ensureStampOnPublishPlacement: (fields: unknown, scopeLabel?: string) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Check that `maxItems` is a positive whole number on a field that holds items: a `list: true`
|
|
67
|
+
* field or a `block` field. Recurses into groups, objects and block templates like
|
|
68
|
+
* `ensureStampOnPublishPlacement`. Throws on the first problem.
|
|
69
|
+
*/
|
|
70
|
+
export declare const ensureMaxItemsPlacement: (fields: unknown, scopeLabel?: string) => void;
|
|
65
71
|
/**
|
|
66
72
|
* Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
|
|
67
73
|
* `mdxAllow` and `executable` against each other. Throws on the first problem.
|
|
@@ -294,6 +294,39 @@ export const ensureStampOnPublishPlacement = (fields, scopeLabel = 'entry schema
|
|
|
294
294
|
};
|
|
295
295
|
check(fields, undefined);
|
|
296
296
|
};
|
|
297
|
+
/**
|
|
298
|
+
* Check that `maxItems` is a positive whole number on a field that holds items: a `list: true`
|
|
299
|
+
* field or a `block` field. Recurses into groups, objects and block templates like
|
|
300
|
+
* `ensureStampOnPublishPlacement`. Throws on the first problem.
|
|
301
|
+
*/
|
|
302
|
+
export const ensureMaxItemsPlacement = (fields, scopeLabel = 'entry schema') => {
|
|
303
|
+
const refuse = (f, why) => {
|
|
304
|
+
throw new Error(`${scopeLabel}: field "${f.name ?? 'unknown'}" has maxItems, ${why}`);
|
|
305
|
+
};
|
|
306
|
+
const check = (scopeFields) => {
|
|
307
|
+
if (!Array.isArray(scopeFields))
|
|
308
|
+
return;
|
|
309
|
+
for (const field of scopeFields) {
|
|
310
|
+
const f = field;
|
|
311
|
+
if (f?.maxItems !== undefined) {
|
|
312
|
+
if (typeof f.maxItems !== 'number' || !Number.isInteger(f.maxItems) || f.maxItems < 1) {
|
|
313
|
+
refuse(f, 'which must be a positive whole number');
|
|
314
|
+
}
|
|
315
|
+
if (f.list !== true && f.type !== 'block') {
|
|
316
|
+
refuse(f, 'but it is not a list; only a `list: true` field or a block field holds items');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (f?.type === 'group' || f?.type === 'object')
|
|
320
|
+
check(f.fields);
|
|
321
|
+
if (f?.type === 'block' && Array.isArray(f.templates)) {
|
|
322
|
+
for (const template of f.templates) {
|
|
323
|
+
check(template.fields);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
check(fields);
|
|
329
|
+
};
|
|
297
330
|
/**
|
|
298
331
|
* Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
|
|
299
332
|
* `mdxAllow` and `executable` against each other. Throws on the first problem.
|
|
@@ -16,6 +16,7 @@ import { DateTimeField } from './fields/DateTimeField.js';
|
|
|
16
16
|
import { BlockField } from './fields/BlockField.js';
|
|
17
17
|
import { SelectField } from './fields/SelectField.js';
|
|
18
18
|
import { ReferenceField } from './fields/ReferenceField.js';
|
|
19
|
+
import { ReferenceListField } from './fields/ReferenceListField.js';
|
|
19
20
|
import { CodeField } from './fields/CodeField.js';
|
|
20
21
|
import { ImageField } from './fields/ImageField.js';
|
|
21
22
|
import { ObjectField } from './fields/ObjectField.js';
|
|
@@ -201,7 +202,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
201
202
|
switch (field.type) {
|
|
202
203
|
case 'string':
|
|
203
204
|
if (field.list) {
|
|
204
|
-
return wrapWithComments(_jsx(StringListField, { id: fieldId, label: label, description: field.description, value: Array.isArray(currentValue) ? currentValue : [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly }, fieldKey(path)));
|
|
205
|
+
return wrapWithComments(_jsx(StringListField, { id: fieldId, label: label, description: field.description, value: Array.isArray(currentValue) ? currentValue : [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly, maxItems: field.maxItems }, fieldKey(path)));
|
|
205
206
|
}
|
|
206
207
|
return wrapWithComments(_jsx(TextField, { id: fieldId, label: label, description: field.description, value: currentValue ?? '', onChange: update, dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly }, fieldKey(path)));
|
|
207
208
|
case 'boolean':
|
|
@@ -210,7 +211,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
210
211
|
if (field.list) {
|
|
211
212
|
return wrapWithComments(_jsx(NumberListField, { id: fieldId, label: label, description: field.description, value: Array.isArray(currentValue)
|
|
212
213
|
? currentValue.filter((v) => typeof v === 'number')
|
|
213
|
-
: [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly }, fieldKey(path)));
|
|
214
|
+
: [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly, maxItems: field.maxItems }, fieldKey(path)));
|
|
214
215
|
}
|
|
215
216
|
return wrapWithComments(_jsx(NumberField, { id: fieldId, label: label, description: field.description, value: typeof currentValue === 'number' ? currentValue : undefined, onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly }, fieldKey(path)));
|
|
216
217
|
case 'datetime':
|
|
@@ -229,22 +230,23 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
229
230
|
? Array.isArray(currentValue)
|
|
230
231
|
? currentValue
|
|
231
232
|
: []
|
|
232
|
-
: (currentValue ?? ''), multiple: isMulti, onChange: (next) => update(next), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly }, fieldKey(path)));
|
|
233
|
+
: (currentValue ?? ''), multiple: isMulti, onChange: (next) => update(next), dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly, maxItems: isMulti ? selectField.maxItems : undefined }, fieldKey(path)));
|
|
233
234
|
}
|
|
234
235
|
case 'reference': {
|
|
235
236
|
const referenceField = field;
|
|
236
237
|
const staticOptions = referenceField.options
|
|
237
238
|
? normalizeOptions(referenceField.options)
|
|
238
239
|
: undefined;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
if (referenceField.list) {
|
|
241
|
+
return wrapWithComments(_jsx(ReferenceListField, { id: fieldId, label: label, description: field.description, required: field.required, options: staticOptions, collections: referenceField.collections, entryTypes: referenceField.entryTypes, displayField: referenceField.displayField, branch: branch, optionSort: referenceField.optionSort, value: Array.isArray(currentValue) ? currentValue : [], onChange: (next) => update(next), path: path, canopyPath: canopyPath, readOnly: readOnly, resetKey: boundaryResetKey, maxItems: referenceField.maxItems, itemError: (index) => fieldErrors?.[normalizeCanopyPath([...path, index])], onRemoved: (index, id, title) => onRemoved?.({
|
|
242
|
+
kind: 'list-item',
|
|
243
|
+
listPath: path,
|
|
244
|
+
index,
|
|
245
|
+
item: id,
|
|
246
|
+
label: `"${title}" from ${label}`,
|
|
247
|
+
}) }, fieldKey(path)));
|
|
248
|
+
}
|
|
249
|
+
return wrapWithComments(_jsx(ReferenceField, { id: fieldId, label: label, description: field.description, options: staticOptions, collections: referenceField.collections, entryTypes: referenceField.entryTypes, displayField: referenceField.displayField, branch: branch, optionSort: referenceField.optionSort, value: currentValue ?? '', onChange: (next) => update(next), dataCanopyField: canopyPath, readOnly: readOnly }, fieldKey(path)));
|
|
248
250
|
}
|
|
249
251
|
case 'image': {
|
|
250
252
|
const imageField = field;
|
|
@@ -256,7 +258,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
256
258
|
}
|
|
257
259
|
case 'block': {
|
|
258
260
|
const blockField = field;
|
|
259
|
-
return wrapWithComments(_jsx(BlockField, { label: label, required: field.required, description: field.description, templates: blockField.templates, value: (Array.isArray(currentValue) ? currentValue : []), onChange: (next) => update(next), renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly, resetKey: boundaryResetKey, hasErrors: rowHasErrors(path), onRemoved: (index, block) => {
|
|
261
|
+
return wrapWithComments(_jsx(BlockField, { label: label, required: field.required, description: field.description, templates: blockField.templates, value: (Array.isArray(currentValue) ? currentValue : []), onChange: (next) => update(next), renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path), readOnly: readOnly, resetKey: boundaryResetKey, hasErrors: rowHasErrors(path), maxItems: blockField.maxItems, onRemoved: (index, block) => {
|
|
260
262
|
const template = blockField.templates.find((t) => t.name === block.template);
|
|
261
263
|
onRemoved?.({
|
|
262
264
|
kind: 'list-item',
|
package/dist/editor/copy.d.ts
CHANGED
|
@@ -19,3 +19,8 @@ export declare const EDITOR_ACTIONS: {
|
|
|
19
19
|
readonly changeUrl: "Change URL";
|
|
20
20
|
readonly changeSlug: "Change slug";
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Why a list at its `maxItems` adds nothing more, as a disabled Add's Tooltip or an input's hint.
|
|
24
|
+
* `one` and `many` name what the list holds.
|
|
25
|
+
*/
|
|
26
|
+
export declare const listFullReason: (maxItems: number, one?: string, many?: string) => string;
|
package/dist/editor/copy.js
CHANGED
|
@@ -19,3 +19,8 @@ export const EDITOR_ACTIONS = {
|
|
|
19
19
|
changeUrl: 'Change URL',
|
|
20
20
|
changeSlug: 'Change slug',
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Why a list at its `maxItems` adds nothing more, as a disabled Add's Tooltip or an input's hint.
|
|
24
|
+
* `one` and `many` name what the list holds.
|
|
25
|
+
*/
|
|
26
|
+
export const listFullReason = (maxItems, one = 'item', many = 'items') => `This list holds at most ${maxItems} ${maxItems === 1 ? one : many}. Remove one to add another.`;
|
|
@@ -26,6 +26,8 @@ export interface BlockFieldProps {
|
|
|
26
26
|
hasErrors?: (index: number) => boolean;
|
|
27
27
|
/** Told of each block Remove takes out, so the caller can offer Undo. */
|
|
28
28
|
onRemoved?: (index: number, block: BlockInstance) => void;
|
|
29
|
+
/** The most blocks the list holds; at it, every add control is disabled and says why. */
|
|
30
|
+
maxItems?: number;
|
|
29
31
|
}
|
|
30
32
|
export declare const BlockField: React.FC<BlockFieldProps>;
|
|
31
33
|
export {};
|